@tailor-platform/sdk 1.40.0 → 1.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +23 -0
  3. package/dist/{actor-B2oEmlTc.d.mts → actor-DzCuoMlP.d.mts} +2 -2
  4. package/dist/{application-C_LFXkKJ.mjs → application-DQpD_kHR.mjs} +88 -9
  5. package/dist/application-DQpD_kHR.mjs.map +1 -0
  6. package/dist/application-DUcmoFdc.mjs +4 -0
  7. package/dist/brand-Ll48SMXe.mjs.map +1 -1
  8. package/dist/cli/index.mjs +34 -29
  9. package/dist/cli/index.mjs.map +1 -1
  10. package/dist/cli/lib.d.mts +49 -7
  11. package/dist/cli/lib.mjs +4 -4
  12. package/dist/cli/lib.mjs.map +1 -1
  13. package/dist/{client-DjGFRjH4.mjs → client-CcV6Jjds.mjs} +8 -5
  14. package/dist/{client-DjGFRjH4.mjs.map → client-CcV6Jjds.mjs.map} +1 -1
  15. package/dist/{client-Dtf48x0o.mjs → client-Cn9SqhZT.mjs} +1 -1
  16. package/dist/configure/index.d.mts +5 -5
  17. package/dist/configure/index.mjs +83 -3
  18. package/dist/configure/index.mjs.map +1 -1
  19. package/dist/{crash-report-CEIXtw4D.mjs → crash-report-CSYupJ0T.mjs} +1 -1
  20. package/dist/{crash-report-CSWITsTz.mjs → crash-report-CUbm1ErM.mjs} +2 -2
  21. package/dist/{crash-report-CSWITsTz.mjs.map → crash-report-CUbm1ErM.mjs.map} +1 -1
  22. package/dist/{index-Chvw1Eod.d.mts → index-0Dk-fDWi.d.mts} +2 -2
  23. package/dist/{index-CiNNNpuH.d.mts → index-BEEL1-6Z.d.mts} +2 -2
  24. package/dist/{index-D_ezppY7.d.mts → index-Br4XCvX1.d.mts} +103 -86
  25. package/dist/{index-BtXZdz-F.d.mts → index-DdsUV-aA.d.mts} +2 -2
  26. package/dist/{index-reFAYSX7.d.mts → index-ZZYEd_0R.d.mts} +2 -2
  27. package/dist/{job-p6zf8Qpg.mjs → job-BOvKyNdT.mjs} +15 -9
  28. package/dist/job-BOvKyNdT.mjs.map +1 -0
  29. package/dist/plugin/builtin/enum-constants/index.d.mts +1 -1
  30. package/dist/plugin/builtin/file-utils/index.d.mts +1 -1
  31. package/dist/plugin/builtin/kysely-type/index.d.mts +1 -1
  32. package/dist/plugin/builtin/seed/index.d.mts +1 -1
  33. package/dist/plugin/index.d.mts +2 -2
  34. package/dist/{runtime-im7Sq4jO.mjs → runtime-Bn68JXnL.mjs} +199 -79
  35. package/dist/runtime-Bn68JXnL.mjs.map +1 -0
  36. package/dist/{tailor-db-field-CoFKRCYW.d.mts → tailor-db-field-D_z185oq.d.mts} +36 -6
  37. package/dist/utils/test/index.d.mts +36 -3
  38. package/dist/utils/test/index.mjs +78 -9
  39. package/dist/utils/test/index.mjs.map +1 -1
  40. package/dist/{workflow.generated-Btz6srLR.d.mts → workflow.generated-CDCnZNkH.d.mts} +2 -2
  41. package/docs/cli/function.md +83 -1
  42. package/docs/cli-reference.md +3 -1
  43. package/docs/services/executor.md +4 -0
  44. package/docs/services/idp.md +93 -56
  45. package/docs/services/resolver.md +4 -2
  46. package/docs/services/workflow.md +117 -3
  47. package/docs/testing.md +95 -7
  48. package/package.json +23 -23
  49. package/dist/application-CEeKm4R-.mjs +0 -4
  50. package/dist/application-C_LFXkKJ.mjs.map +0 -1
  51. package/dist/job-p6zf8Qpg.mjs.map +0 -1
  52. package/dist/runtime-im7Sq4jO.mjs.map +0 -1
@@ -26,14 +26,26 @@ Configure the Built-in IdP using `defineIdp()`:
26
26
  import { defineIdp, defineConfig } from "@tailor-platform/sdk";
27
27
 
28
28
  const idp = defineIdp("my-idp", {
29
- authorization: "loggedIn",
30
29
  clients: ["my-client"],
30
+ permission: {
31
+ create: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
32
+ read: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
33
+ update: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
34
+ delete: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
35
+ sendPasswordResetEmail: [{ conditions: [], permit: false }],
36
+ },
31
37
  });
32
38
 
33
39
  // You can define multiple IdPs
34
40
  const anotherIdp = defineIdp("another-idp", {
35
- authorization: "loggedIn",
36
41
  clients: ["another-client"],
42
+ permission: {
43
+ create: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
44
+ read: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
45
+ update: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
46
+ delete: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
47
+ sendPasswordResetEmail: [{ conditions: [], permit: false }],
48
+ },
37
49
  });
38
50
 
39
51
  export default defineConfig({
@@ -43,30 +55,60 @@ export default defineConfig({
43
55
 
44
56
  ## Options
45
57
 
46
- ### authorization (optional)
58
+ ### permission
47
59
 
48
- User management permissions. Controls who can manage users in the IdP. This field can be omitted when using `permission` for access control.
60
+ Per-operation permission policies for IdP user management. Controls who can create, read, update, delete users, and send password reset emails.
49
61
 
50
62
  ```typescript
51
63
  defineIdp("my-idp", {
52
- authorization: "loggedIn", // Only logged-in users can manage
64
+ clients: ["my-client"],
65
+ permission: {
66
+ create: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
67
+ read: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
68
+ update: [
69
+ {
70
+ conditions: [
71
+ [{ user: "role" }, "=", "ADMIN"],
72
+ [{ newIdpUser: "name" }, "!=", { oldIdpUser: "name" }],
73
+ ],
74
+ permit: true,
75
+ },
76
+ ],
77
+ delete: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
78
+ sendPasswordResetEmail: [{ conditions: [], permit: false }],
79
+ },
53
80
  });
81
+ ```
54
82
 
55
- defineIdp("my-idp", {
56
- authorization: "insecure", // Anyone can manage (development only)
57
- });
83
+ **Operations:**
84
+
85
+ - `create` - Controls who can create IdP users
86
+ - `read` - Controls who can read IdP users
87
+ - `update` - Controls who can update IdP users
88
+ - `delete` - Controls who can delete IdP users
89
+ - `sendPasswordResetEmail` - Controls who can send password reset emails. The examples above disable this operation; to enable it, use a permission such as `[{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }]`.
90
+
91
+ **Operands:**
92
+
93
+ - `{ user: "field" }` - Authenticated user's attribute. Built-in fields: `"id"` (user ID), `"_loggedIn"` (boolean, whether the user is authenticated). User-defined attributes (e.g., `"role"`) are also available when configured via `userProfile.attributes` or `machineUserAttributes` in `defineAuth()`
94
+ - `{ idpUser: "field" }` - IdP user field (for create/read/delete). Allowed values: `"id"`, `"name"`, `"disabled"`
95
+ - `{ oldIdpUser: "field" }` - Previous IdP user field value (for update only). Allowed values: `"id"`, `"name"`, `"disabled"`
96
+ - `{ newIdpUser: "field" }` - New IdP user field value (for update only). Allowed values: `"id"`, `"name"`, `"disabled"`
97
+ - Literal values: `string`, `boolean`, `string[]`, `boolean[]`
98
+
99
+ **Operators:** `"="`, `"!="`, `"in"`, `"not in"`
100
+
101
+ **Helper:** `unsafeAllowAllIdPPermission` grants full access without conditions. Intended only for development and testing.
102
+
103
+ ```typescript
104
+ import { unsafeAllowAllIdPPermission } from "@tailor-platform/sdk";
58
105
 
59
106
  defineIdp("my-idp", {
60
- authorization: { cel: "user.role == 'admin'" }, // CEL expression
107
+ clients: ["my-client"],
108
+ permission: unsafeAllowAllIdPPermission,
61
109
  });
62
110
  ```
63
111
 
64
- **Values:**
65
-
66
- - `"insecure"` - No authentication required (use only for development)
67
- - `"loggedIn"` - Requires authenticated user
68
- - `{ cel: "<expression>" }` - Custom authorization logic using CEL
69
-
70
112
  ### clients
71
113
 
72
114
  OAuth client names that can use this IdP:
@@ -77,78 +119,67 @@ defineIdp("my-idp", {
77
119
  });
78
120
  ```
79
121
 
80
- ### emailConfig
122
+ ### authorization (optional, legacy)
81
123
 
82
- Namespace-level email configuration defaults. Per-request values take priority over these defaults.
124
+ Legacy access control field. Use `permission` instead for fine-grained per-operation control. This field is kept for backward compatibility.
83
125
 
84
126
  ```typescript
85
127
  defineIdp("my-idp", {
86
- authorization: "loggedIn",
87
- clients: ["my-client"],
88
- emailConfig: {
89
- fromName: "My App",
90
- passwordResetSubject: "Reset your password",
91
- },
128
+ clients: ["default-client"],
129
+ authorization: "loggedIn", // Only logged-in users can manage
92
130
  });
93
131
  ```
94
132
 
95
- **Fields:**
96
-
97
- - `fromName` - Default sender display name for emails. Empty means use mailer default.
98
- - `passwordResetSubject` - Default subject for password reset emails. Empty means use localized default.
133
+ **Values:**
99
134
 
100
- **Validation:** Each field must be 200 characters or less and must not contain newline characters.
135
+ - `"insecure"` - No authentication required (use only for development)
136
+ - `"loggedIn"` - Requires authenticated user
137
+ - `{ cel: "<expression>" }` - Custom authorization logic using CEL
101
138
 
102
- ### permission
139
+ ### emailConfig
103
140
 
104
- Per-operation permission policies for IdP user management. Controls who can create, read, update, delete users, and send password reset emails.
141
+ Namespace-level email configuration defaults. Per-request values take priority over these defaults.
105
142
 
106
143
  ```typescript
107
144
  defineIdp("my-idp", {
108
- authorization: "loggedIn",
109
145
  clients: ["my-client"],
110
146
  permission: {
111
147
  create: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
112
- read: [{ conditions: [[{ user: "_loggedIn" }, "=", true]], permit: true }],
113
- update: [
114
- { conditions: [[{ newIdpUser: "name" }, "!=", { oldIdpUser: "name" }]], permit: true },
115
- ],
148
+ read: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
149
+ update: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
116
150
  delete: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
117
- sendPasswordResetEmail: [{ conditions: [], permit: true }],
151
+ sendPasswordResetEmail: [{ conditions: [], permit: false }],
152
+ },
153
+ emailConfig: {
154
+ fromName: "My App",
155
+ passwordResetSubject: "Reset your password",
118
156
  },
119
157
  });
120
158
  ```
121
159
 
122
- **Operations:**
123
-
124
- - `create` - Controls who can create IdP users
125
- - `read` - Controls who can read IdP users
126
- - `update` - Controls who can update IdP users
127
- - `delete` - Controls who can delete IdP users
128
- - `sendPasswordResetEmail` - Controls who can send password reset emails
160
+ **Fields:**
129
161
 
130
- **Operands:**
162
+ - `fromName` - Default sender display name for emails. Empty means use mailer default.
163
+ - `passwordResetSubject` - Default subject for password reset emails. Empty means use localized default.
131
164
 
132
- - `{ user: "field" }` - Authenticated user's attribute
133
- - `{ idpUser: "field" }` - IdP user field (for create/read/delete). Allowed values: `"id"`, `"name"`, `"disabled"`
134
- - `{ oldIdpUser: "field" }` - Previous IdP user field value (for update only). Allowed values: `"id"`, `"name"`, `"disabled"`
135
- - `{ newIdpUser: "field" }` - New IdP user field value (for update only). Allowed values: `"id"`, `"name"`, `"disabled"`
136
- - Literal values: `string`, `boolean`, `string[]`, `boolean[]`
165
+ **Validation:** Each field must be 200 characters or less and must not contain newline characters.
137
166
 
138
- **Operators:** `"="`, `"!="`, `"in"`, `"not in"`
167
+ ### publishUserEvents
139
168
 
140
- **Helper:** `unsafeAllowAllIdPPermission` grants full access without conditions. Intended only for development and testing.
169
+ Publish IdP user lifecycle events (`idp.user.created`, `idp.user.updated`, `idp.user.deleted`). These events are consumed by executors that use `idpUserCreatedTrigger`, `idpUserUpdatedTrigger`, `idpUserDeletedTrigger`, or `idpUserTrigger`.
141
170
 
142
171
  ```typescript
143
- import { unsafeAllowAllIdPPermission } from "@tailor-platform/sdk";
144
-
145
172
  defineIdp("my-idp", {
146
- authorization: "loggedIn",
147
173
  clients: ["my-client"],
148
- permission: unsafeAllowAllIdPPermission,
174
+ publishUserEvents: true,
149
175
  });
150
176
  ```
151
177
 
178
+ **Auto-configuration:** When `publishUserEvents` is omitted, the SDK enables it automatically during `apply` if the project defines any executor with an `idpUser` trigger. Set the value explicitly to override:
179
+
180
+ - `publishUserEvents: true`: always publish events.
181
+ - `publishUserEvents: false`: never publish events. The SDK warns when executors with `idpUser` triggers are present, since those executors will not fire for this IdP.
182
+
152
183
  ## Using idp.provider()
153
184
 
154
185
  The `idp.provider()` method creates a type-safe reference to the IdP for use in Auth configuration. The client name is validated at compile time against the clients defined in the IdP.
@@ -158,8 +189,14 @@ import { defineIdp, defineAuth, defineConfig } from "@tailor-platform/sdk";
158
189
  import { user } from "./tailordb/user";
159
190
 
160
191
  const idp = defineIdp("my-idp", {
161
- authorization: "loggedIn",
162
192
  clients: ["default-client", "mobile-client"],
193
+ permission: {
194
+ create: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
195
+ read: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
196
+ update: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
197
+ delete: [{ conditions: [[{ user: "role" }, "=", "ADMIN"]], permit: true }],
198
+ sendPasswordResetEmail: [{ conditions: [], permit: false }],
199
+ },
163
200
  });
164
201
 
165
202
  const auth = defineAuth("my-auth", {
@@ -234,7 +234,9 @@ Validation runs automatically before the `body` function executes. When validati
234
234
  Define actual resolver logic in the `body` function. Function arguments include:
235
235
 
236
236
  - `input` - Input data from GraphQL request
237
- - `user` - User performing the operation
237
+ - `user` - The user who called this resolver; unaffected by `authInvoker`
238
+ - `invoker` - The principal running this function; equals `user` by default, or the machine user set by `authInvoker`. `null` for anonymous calls.
239
+ - `env` - Environment variables declared in `tailor.config.ts`
238
240
 
239
241
  ### Using Kysely for Database Access
240
242
 
@@ -371,4 +373,4 @@ The machine user name is looked up in the auth service configured on your app (`
371
373
 
372
374
  > **Deprecated:** `auth.invoker("batch-processor")` still works, but is deprecated. Importing `auth` into runtime files pulls config-layer (Node-only) dependencies into the bundle.
373
375
 
374
- **Note:** `authInvoker` controls the permissions for database operations and other platform actions, but the `user` object passed to the `body` function still reflects the original caller who invoked the resolver.
376
+ **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.
@@ -60,7 +60,7 @@ export const fetchCustomer = createWorkflowJob({
60
60
 
61
61
  Workflow job inputs and outputs are serialized as JSON when passed between jobs. This imposes type constraints:
62
62
 
63
- **Input types** must be JSON-compatible — only primitives (`string`, `number`, `boolean`, `null`), arrays, and plain objects are allowed. `Date`, `Map`, `Set`, functions, and other non-serializable types cannot be used.
63
+ **Input types** must be JSON-compatible — primitives (`string`, `number`, `boolean`), arrays, and plain objects are allowed. `Date`, `Map`, `Set`, functions, and other non-serializable types cannot be used. Top-level `null` is also rejected because the platform normalizes top-level `null`/`undefined` args to `{}` (nested `null` inside objects or arrays is preserved).
64
64
 
65
65
  ```typescript
66
66
  // OK
@@ -78,9 +78,17 @@ export const badJob = createWorkflowJob({
78
78
  // ...
79
79
  },
80
80
  });
81
+
82
+ // Compile error — top-level null would be normalized to {} by the platform
83
+ export const nullJob = createWorkflowJob({
84
+ name: "null-job",
85
+ body: async (input: { id: string } | null) => {
86
+ // ...
87
+ },
88
+ });
81
89
  ```
82
90
 
83
- **Output types** are more permissive `Date` and objects with `toJSON()` are allowed because they are serialized via `JSON.stringify` at runtime (e.g., `Date` becomes a string).
91
+ **Output types** have the same restriction as inputs: must be JsonValue-compatible (plain objects/arrays; no class instances or functions). Values with methods (function-typed properties) are rejected at compile time this covers class instances like `Date` or `RegExp` as well as any plain object that exposes a method such as `toJSON()`.
84
92
 
85
93
  These constraints are enforced at compile time — you will get a type error if you use an unsupported type.
86
94
 
@@ -175,8 +183,10 @@ import { sendNotification } from "./jobs/send-notification";
175
183
  // Jobs must be named exports
176
184
  export const processOrder = createWorkflowJob({
177
185
  name: "process-order",
178
- body: async (input: { customerId: string }, { env }) => {
186
+ body: async (input: { customerId: string }, { env, invoker }) => {
179
187
  // `env` contains values from `tailor.config.ts` -> `env`.
188
+ // `invoker` is the principal running this job, overridden by `authInvoker`
189
+ // when set; `null` for anonymous calls.
180
190
  // Trigger other jobs by calling .trigger() on the job object.
181
191
  const customer = await fetchCustomer.trigger({
182
192
  customerId: input.customerId,
@@ -196,6 +206,110 @@ export default createWorkflow({
196
206
  });
197
207
  ```
198
208
 
209
+ ## Wait Points
210
+
211
+ Wait points allow a workflow job to suspend execution and wait for an external signal before resuming. This enables human-in-the-loop patterns such as approvals, reviews, and manual confirmations.
212
+
213
+ ### Defining Wait Points
214
+
215
+ Use `defineWaitPoint` to declare a single typed wait point:
216
+
217
+ ```typescript
218
+ import { defineWaitPoint } from "@tailor-platform/sdk";
219
+
220
+ export const approval = defineWaitPoint<
221
+ { message: string; requestId: string },
222
+ { approved: boolean }
223
+ >("approval");
224
+ ```
225
+
226
+ 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:
227
+
228
+ ```typescript
229
+ import { defineWaitPoints } from "@tailor-platform/sdk";
230
+
231
+ export const waitPoints = defineWaitPoints((define) => ({
232
+ /** Manager approval step */
233
+ managerApproval: define<{ amount: number }, { approved: boolean }>(),
234
+ /** Finance review step */
235
+ financeReview: define<{ invoiceId: string }, { validated: boolean }>(),
236
+ }));
237
+
238
+ await waitPoints.managerApproval.wait({ amount: 50000 });
239
+ ```
240
+
241
+ Both accept two type parameters:
242
+
243
+ - **`Payload`** — Data sent when the job suspends (passed to `.wait()`). Must be a pure JSON value (`string`, `number`, `boolean`, `null`, arrays, plain objects). Use `undefined` if no payload is needed.
244
+ - **`Result`** — Data returned when the wait point is resolved (returned from `.wait()`, produced by the `.resolve()` callback). Must be a pure JSON value.
245
+
246
+ Both must be JsonValue-compatible (plain objects/arrays; no class instances or functions). Values with methods (function-typed properties) are rejected at compile time — this covers class instances like `Date` or `RegExp` as well as any plain object that exposes a method such as `toJSON()`. Convert such values to `string` (e.g. ISO strings) or `number` (epoch millis) before passing them through a wait point.
247
+
248
+ ### Waiting in a Job
249
+
250
+ Call `.wait()` inside a workflow job body to suspend execution:
251
+
252
+ ```typescript
253
+ import { createWorkflow, createWorkflowJob, defineWaitPoint } from "@tailor-platform/sdk";
254
+
255
+ export const approval = defineWaitPoint<
256
+ { message: string; requestId: string },
257
+ { approved: boolean }
258
+ >("approval");
259
+
260
+ export const processWithApproval = createWorkflowJob({
261
+ name: "process-with-approval",
262
+ body: async (input: { orderId: string }) => {
263
+ // Suspends here until resolved externally
264
+ const result = await approval.wait({
265
+ message: `Please approve order ${input.orderId}`,
266
+ requestId: input.orderId,
267
+ });
268
+
269
+ if (!result.approved) {
270
+ return { orderId: input.orderId, status: "rejected" as const };
271
+ }
272
+ return { orderId: input.orderId, status: "approved" as const };
273
+ },
274
+ });
275
+
276
+ export default createWorkflow({
277
+ name: "approval-workflow",
278
+ mainJob: processWithApproval,
279
+ });
280
+ ```
281
+
282
+ ### Resolving from a Resolver
283
+
284
+ Call `.resolve()` from a resolver (or executor) to resume a suspended job. The callback receives the payload that was passed to `.wait()` and returns the result:
285
+
286
+ ```typescript
287
+ import { createResolver, t } from "@tailor-platform/sdk";
288
+ import { approval } from "../workflows/approval";
289
+
290
+ export default createResolver({
291
+ name: "resolveApproval",
292
+ description: "Resolve a waiting approval",
293
+ operation: "mutation",
294
+ input: {
295
+ executionId: t.string(),
296
+ approved: t.bool(),
297
+ },
298
+ body: async ({ input }) => {
299
+ await approval.resolve(input.executionId, (payload) => {
300
+ console.log("Resolving:", payload.message);
301
+ return { approved: input.approved };
302
+ });
303
+ return { resolved: true };
304
+ },
305
+ output: t.object({
306
+ resolved: t.bool(),
307
+ }),
308
+ });
309
+ ```
310
+
311
+ Wait points can be imported and used in any file (workflow jobs, resolvers, executors). For local testing, see [Testing Wait Points](../testing.md#testing-wait-points).
312
+
199
313
  ## Retry Policy
200
314
 
201
315
  You can configure automatic retry behavior with exponential backoff by setting `retryPolicy` on a workflow. All fields are required when `retryPolicy` is set:
package/docs/testing.md CHANGED
@@ -10,7 +10,7 @@ npm create @tailor-platform/sdk -- --template testing <your-project-name>
10
10
 
11
11
  ## Unit Tests
12
12
 
13
- Unit tests verify resolver logic without requiring deployment.
13
+ Unit tests verify resolver and workflow logic locally without requiring deployment.
14
14
 
15
15
  ### Simple Resolver Testing
16
16
 
@@ -171,13 +171,52 @@ describe("decrementUserAge resolver", () => {
171
171
  - Mock high-level operations instead of low-level SQL queries
172
172
  - **Best for:** Complex business logic with multiple database operations
173
173
 
174
- ## Workflow Tests
174
+ ### Testing Resolvers that Call `.resolve()`
175
175
 
176
- Test workflows locally without deploying to Tailor Platform.
176
+ Use `setupWaitPointMock` to mock `tailor.workflow.resolve` when testing resolvers that resume a suspended workflow execution.
177
177
 
178
- ### Job Unit Tests
178
+ ```typescript
179
+ import { afterEach } from "vitest";
180
+ import { setupWaitPointMock, unauthenticatedTailorUser } from "@tailor-platform/sdk/test";
181
+ import resolver from "./resolvers/resolveApproval";
182
+
183
+ const TailorGlobal = globalThis as { tailor?: { workflow?: Record<string, unknown> } };
184
+
185
+ describe("resolveApproval resolver", () => {
186
+ afterEach(() => {
187
+ delete TailorGlobal.tailor;
188
+ });
189
+
190
+ test("resolves approval", async () => {
191
+ const { resolveCalls } = setupWaitPointMock({
192
+ onResolve: (_execId, _key, callback) => {
193
+ const result = callback({ message: "Please approve", orderId: "order-1" });
194
+ expect(result).toEqual({ approved: true });
195
+ },
196
+ });
197
+
198
+ const result = await resolver.body({
199
+ input: { executionId: "exec-1", approved: true },
200
+ user: unauthenticatedTailorUser,
201
+ env: {},
202
+ });
179
203
 
180
- Test individual job logic by calling `.body()` directly:
204
+ expect(result).toEqual({ resolved: true });
205
+ expect(resolveCalls).toHaveLength(1);
206
+ expect(resolveCalls[0]).toEqual({ executionId: "exec-1", key: "approval" });
207
+ });
208
+ });
209
+ ```
210
+
211
+ **Key points:**
212
+
213
+ - `onResolve` lets you verify the callback behavior in resolvers that call `.resolve()`
214
+ - Clean up mocks in `afterEach` by deleting `TailorGlobal.tailor`
215
+ - **Best for:** Resolvers that resume suspended workflow executions
216
+
217
+ ### Workflow Job Unit Tests
218
+
219
+ Test individual workflow job logic locally without deploying. Call `.body()` directly:
181
220
 
182
221
  ```typescript
183
222
  import workflow, { addNumbers, calculate } from "./workflows/calculation";
@@ -218,9 +257,9 @@ describe("workflow with dependencies", () => {
218
257
 
219
258
  **Note:** To execute dependent jobs without mocking, and they require `env`, use `vi.stubEnv(WORKFLOW_TEST_ENV_KEY, ...)` and call `.trigger()` directly as shown in the integration test section below.
220
259
 
221
- ### Integration Tests with `.trigger()`
260
+ ### Workflow Integration Tests with `.trigger()`
222
261
 
223
- Test the full workflow execution using `workflow.mainJob.trigger()`:
262
+ Test the full workflow execution locally using `workflow.mainJob.trigger()`:
224
263
 
225
264
  ```typescript
226
265
  import { WORKFLOW_TEST_ENV_KEY } from "@tailor-platform/sdk/test";
@@ -252,6 +291,55 @@ describe("workflow integration", () => {
252
291
  - Use `workflow.mainJob.trigger()` to execute the full workflow chain and get the result
253
292
  - **Best for:** Testing workflow orchestration and job dependencies
254
293
 
294
+ ### Testing Jobs with Wait Points
295
+
296
+ Use `setupWaitPointMock` to mock `tailor.workflow.wait` when testing jobs that suspend on wait points:
297
+
298
+ ```typescript
299
+ import { afterEach, vi } from "vitest";
300
+ import { setupWaitPointMock } from "@tailor-platform/sdk/test";
301
+ import { processWithApproval } from "./workflows/approval";
302
+
303
+ const TailorGlobal = globalThis as { tailor?: { workflow?: Record<string, unknown> } };
304
+
305
+ describe("approval workflow", () => {
306
+ afterEach(() => {
307
+ delete TailorGlobal.tailor;
308
+ });
309
+
310
+ test("approved flow returns approved status", async () => {
311
+ const { waitCalls } = setupWaitPointMock({
312
+ onWait: (_key, _payload) => ({ approved: true }),
313
+ });
314
+
315
+ const result = await processWithApproval.body({ orderId: "order-1" }, { env: {} });
316
+
317
+ expect(result).toEqual({ orderId: "order-1", status: "approved" });
318
+ expect(waitCalls).toHaveLength(1);
319
+ expect(waitCalls[0]).toEqual({
320
+ key: "approval",
321
+ payload: { message: "Please approve order order-1", orderId: "order-1" },
322
+ });
323
+ });
324
+
325
+ test("rejected flow returns rejected status", async () => {
326
+ setupWaitPointMock({
327
+ onWait: () => ({ approved: false }),
328
+ });
329
+
330
+ const result = await processWithApproval.body({ orderId: "order-2" }, { env: {} });
331
+
332
+ expect(result).toEqual({ orderId: "order-2", status: "rejected" });
333
+ });
334
+ });
335
+ ```
336
+
337
+ **Key points:**
338
+
339
+ - `onWait` controls what `.wait()` returns — use it to test different branches (approved/rejected)
340
+ - Clean up mocks in `afterEach` by deleting `TailorGlobal.tailor`
341
+ - **Best for:** Jobs that suspend on wait points for human-in-the-loop approval
342
+
255
343
  ## End-to-End (E2E) Tests
256
344
 
257
345
  E2E tests verify your application works correctly when deployed to Tailor Platform. They test the full stack including GraphQL API, database operations, and authentication.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailor-platform/sdk",
3
- "version": "1.40.0",
3
+ "version": "1.43.0",
4
4
  "description": "Tailor Platform SDK - The SDK to work with Tailor Platform",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -81,22 +81,22 @@
81
81
  "@bufbuild/protobuf": "2.11.0",
82
82
  "@connectrpc/connect": "2.1.1",
83
83
  "@connectrpc/connect-node": "2.1.1",
84
- "@inquirer/core": "11.1.8",
85
- "@inquirer/prompts": "8.4.1",
84
+ "@inquirer/core": "11.1.9",
85
+ "@inquirer/prompts": "8.4.2",
86
86
  "@jridgewell/trace-mapping": "0.3.31",
87
87
  "@liam-hq/cli": "0.7.24",
88
88
  "@napi-rs/keyring": "1.2.0",
89
89
  "@opentelemetry/api": "1.9.1",
90
- "@opentelemetry/exporter-trace-otlp-proto": "0.214.0",
91
- "@opentelemetry/resources": "2.6.1",
92
- "@opentelemetry/sdk-trace-node": "2.6.1",
90
+ "@opentelemetry/exporter-trace-otlp-proto": "0.215.0",
91
+ "@opentelemetry/resources": "2.7.0",
92
+ "@opentelemetry/sdk-trace-node": "2.7.0",
93
93
  "@opentelemetry/semantic-conventions": "1.40.0",
94
- "@oxc-project/types": "0.126.0",
94
+ "@oxc-project/types": "0.127.0",
95
95
  "@standard-schema/spec": "1.1.0",
96
96
  "@tailor-platform/function-kysely-tailordb": "0.1.3",
97
- "@tailor-platform/function-types": "0.8.4",
98
- "@toiroakr/lines-db": "0.9.1",
99
- "@toiroakr/read-multiline": "0.3.0",
97
+ "@tailor-platform/function-types": "0.8.5",
98
+ "@toiroakr/lines-db": "0.9.2",
99
+ "@toiroakr/read-multiline": "0.3.2",
100
100
  "@urql/core": "6.0.1",
101
101
  "chalk": "5.6.2",
102
102
  "chokidar": "5.0.0",
@@ -113,43 +113,43 @@
113
113
  "multiline-ts": "4.0.1",
114
114
  "open": "11.0.0",
115
115
  "ora": "9.3.0",
116
- "oxc-parser": "0.126.0",
116
+ "oxc-parser": "0.127.0",
117
117
  "p-limit": "7.3.0",
118
118
  "pathe": "2.0.3",
119
119
  "pgsql-ast-parser": "12.0.2",
120
120
  "pkg-types": "2.3.0",
121
121
  "politty": "0.4.14",
122
- "rolldown": "1.0.0-rc.15",
122
+ "rolldown": "1.0.0-rc.16",
123
123
  "semver": "7.7.4",
124
124
  "serve": "14.2.6",
125
125
  "sql-highlight": "6.1.0",
126
- "std-env": "4.0.0",
126
+ "std-env": "4.1.0",
127
127
  "table": "6.9.0",
128
128
  "ts-cron-validator": "1.1.5",
129
129
  "tsx": "4.21.0",
130
- "type-fest": "5.5.0",
130
+ "type-fest": "5.6.0",
131
131
  "xdg-basedir": "5.1.0",
132
132
  "zod": "4.3.6"
133
133
  },
134
134
  "devDependencies": {
135
135
  "@eslint/js": "10.0.1",
136
- "@opentelemetry/sdk-trace-base": "2.6.1",
136
+ "@opentelemetry/sdk-trace-base": "2.7.0",
137
137
  "@types/madge": "5.0.3",
138
138
  "@types/mime-types": "3.0.1",
139
139
  "@types/node": "24.12.2",
140
140
  "@types/semver": "7.7.1",
141
- "@typescript/native-preview": "7.0.0-dev.20260417.1",
141
+ "@typescript/native-preview": "7.0.0-dev.20260423.1",
142
142
  "@vitest/coverage-v8": "4.1.4",
143
- "eslint": "10.2.0",
143
+ "eslint": "10.2.1",
144
144
  "eslint-plugin-jsdoc": "62.9.0",
145
- "eslint-plugin-oxlint": "1.60.0",
146
- "oxfmt": "0.45.0",
147
- "oxlint": "1.60.0",
148
- "oxlint-tsgolint": "0.20.0",
145
+ "eslint-plugin-oxlint": "1.61.0",
146
+ "oxfmt": "0.46.0",
147
+ "oxlint": "1.61.0",
148
+ "oxlint-tsgolint": "0.21.1",
149
149
  "sonda": "0.11.1",
150
- "tsdown": "0.21.8",
150
+ "tsdown": "0.21.9",
151
151
  "typescript": "5.9.3",
152
- "typescript-eslint": "8.58.2",
152
+ "typescript-eslint": "8.59.0",
153
153
  "vitest": "4.1.4",
154
154
  "zinfer": "0.1.8"
155
155
  },
@@ -1,4 +0,0 @@
1
-
2
- import { n as generatePluginFilesIfNeeded, r as loadApplication, t as defineApplication } from "./application-C_LFXkKJ.mjs";
3
-
4
- export { defineApplication };