@tailor-platform/sdk 0.16.3 → 0.17.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.
@@ -1,609 +0,0 @@
1
- # Conceptual Guides
2
-
3
- ### TailorDB Concepts
4
-
5
- Define TailorDB Types in files matching glob patterns specified in `tailor.config.ts`.
6
-
7
- #### Field Types
8
-
9
- Define TailorDB Fields using methods like `db.string()`, `db.int()`, etc. All TailorDB Field types are supported:
10
-
11
- | Method | TailorDB | TypeScript |
12
- | --------------- | -------- | -------------- |
13
- | `db.string()` | String | string |
14
- | `db.int()` | Integer | number |
15
- | `db.float()` | Float | number |
16
- | `db.bool()` | Boolean | boolean |
17
- | `db.date()` | Date | string |
18
- | `db.datetime()` | DateTime | string \| Date |
19
- | `db.time()` | Time | string |
20
- | `db.uuid()` | UUID | string |
21
- | `db.enum()` | Enum | string |
22
- | `db.object()` | Nested | object |
23
-
24
- **Enum fields** - specify allowed values as arguments:
25
-
26
- ```typescript
27
- db.enum("red", "green", "blue");
28
- db.enum(
29
- { value: "active", description: "Active status" },
30
- { value: "inactive", description: "Inactive status" },
31
- );
32
- ```
33
-
34
- **Object fields** - specify field structure as an argument:
35
-
36
- ```typescript
37
- db.object({
38
- street: db.string(),
39
- city: db.string(),
40
- country: db.string(),
41
- });
42
- ```
43
-
44
- #### Optional and Array Fields
45
-
46
- Make fields optional or arrays by specifying options:
47
-
48
- ```typescript
49
- db.string({ optional: true });
50
- db.string({ array: true });
51
- db.string({ optional: true, array: true });
52
- ```
53
-
54
- #### Field Modifiers
55
-
56
- **Description** - Add a description to field:
57
-
58
- ```typescript
59
- db.string().description("User's full name");
60
- ```
61
-
62
- **Index / Unique** - Add an index to field:
63
-
64
- ```typescript
65
- db.string().index();
66
- db.string().unique();
67
- ```
68
-
69
- **Relations** - Add a relation to field with automatic index and foreign key constraint:
70
-
71
- ```typescript
72
- const role = db.type("Role", {
73
- name: db.string(),
74
- });
75
-
76
- const user = db.type("User", {
77
- name: db.string(),
78
- roleId: db.uuid().relation({
79
- type: "n-1",
80
- toward: { type: role },
81
- }),
82
- });
83
- ```
84
-
85
- For one-to-one relations, use `type: "1-1"`:
86
-
87
- ```typescript
88
- const userProfile = db.type("UserProfile", {
89
- userId: db.uuid().relation({
90
- type: "1-1",
91
- toward: { type: user },
92
- }),
93
- bio: db.string(),
94
- });
95
- ```
96
-
97
- For foreign key constraint without creating a relation, use `type: "keyOnly"`:
98
-
99
- ```typescript
100
- const user = db.type("User", {
101
- roleId: db.uuid().relation({
102
- type: "keyOnly",
103
- toward: { type: role },
104
- }),
105
- });
106
- ```
107
-
108
- Create relations against different fields using `toward.key`:
109
-
110
- ```typescript
111
- const user = db.type("User", {
112
- email: db.string().unique(),
113
- });
114
-
115
- const userProfile = db.type("UserProfile", {
116
- userEmail: db.string().relation({
117
- type: "1-1",
118
- toward: { type: user, key: "email" },
119
- }),
120
- });
121
- ```
122
-
123
- Customize relation names using `toward.as` / `backward` options:
124
-
125
- ```typescript
126
- const userProfile = db.type("UserProfile", {
127
- userId: db.uuid().relation({
128
- type: "1-1",
129
- toward: { type: user, as: "base" },
130
- backward: "profile",
131
- }),
132
- });
133
- ```
134
-
135
- **Hooks** - Add hooks to execute functions during data creation or update:
136
-
137
- ```typescript
138
- db.datetime().hooks({
139
- create: () => new Date(),
140
- update: () => new Date(),
141
- });
142
- ```
143
-
144
- Function arguments include: `value` (field value), `data` (entire record value), `user` (user performing the operation).
145
-
146
- **Validation** - Add validation functions to field:
147
-
148
- ```typescript
149
- db.string().validate(
150
- ({ value }) => value.length > 5,
151
- [
152
- ({ value }) => value.length < 10,
153
- "Value must be shorter than 10 characters",
154
- ],
155
- );
156
- ```
157
-
158
- **Vector Search** - Enable field for vector search:
159
-
160
- ```typescript
161
- db.string().vector();
162
- ```
163
-
164
- **Serial / Auto-increment** - Enable field auto-increment:
165
-
166
- ```typescript
167
- db.int().serial({
168
- start: 0,
169
- maxValue: 100,
170
- });
171
-
172
- db.string().serial({
173
- start: 0,
174
- format: "CUST_%d",
175
- });
176
- ```
177
-
178
- **Common Fields** - Add common fields using built-in helpers:
179
-
180
- ```typescript
181
- export const user = db.type("User", {
182
- name: db.string(),
183
- ...db.fields.timestamps(),
184
- });
185
- ```
186
-
187
- #### Type Definition
188
-
189
- Define a TailorDB Type using `db.type()` method:
190
-
191
- ```typescript
192
- db.type("User", {
193
- name: db.string(),
194
- });
195
- ```
196
-
197
- Specify PluralForm by passing an array as first argument:
198
-
199
- ```typescript
200
- db.type(["User", "UserList"], {
201
- name: db.string(),
202
- });
203
- ```
204
-
205
- Pass a description as second argument:
206
-
207
- ```typescript
208
- db.type("User", "User in the system", {
209
- name: db.string(),
210
- description: db.string().optional(),
211
- });
212
- ```
213
-
214
- **Composite Indexes** - Configure composite indexes:
215
-
216
- ```typescript
217
- db.type("User", {
218
- firstName: db.string(),
219
- lastName: db.string(),
220
- }).indexes({
221
- fields: ["firstName", "lastName"],
222
- unique: true,
223
- name: "user_name_idx",
224
- });
225
- ```
226
-
227
- **File Fields** - Add file fields:
228
-
229
- ```typescript
230
- db.type("User", {
231
- name: db.string(),
232
- }).files({
233
- avatar: "profile image",
234
- });
235
- ```
236
-
237
- **Features** - Enable additional features:
238
-
239
- ```typescript
240
- db.type("User", {
241
- name: db.string(),
242
- }).features({
243
- aggregation: true,
244
- bulkUpsert: true,
245
- });
246
- ```
247
-
248
- **Permissions** - Configure Permission and GQLPermission. For details, see the [TailorDB Permission documentation](https://docs.tailor.tech/guides/tailordb/permission).
249
-
250
- ```typescript
251
- db.type("User", {
252
- name: db.string(),
253
- role: db.enum("admin", "user").index(),
254
- })
255
- .permission({
256
- create: [[{ user: "role" }, "=", "admin"]],
257
- read: [
258
- [{ user: "role" }, "=", "admin"],
259
- [{ record: "id" }, "=", { user: "id" }],
260
- ],
261
- update: [[{ user: "role" }, "=", "admin"]],
262
- delete: [[{ user: "role" }, "=", "admin"]],
263
- })
264
- .gqlPermission([
265
- { conditions: [[{ user: "role" }, "=", "admin"]], actions: "all" },
266
- { conditions: [[{ user: "role" }, "=", "user"]], actions: ["read"] },
267
- ]);
268
- ```
269
-
270
- Following the secure-by-default principle, all operations are denied if permissions are not configured.
271
-
272
- ### Resolver Concepts
273
-
274
- Define Resolvers in files matching glob patterns specified in `tailor.config.ts`.
275
-
276
- #### Resolver Creation
277
-
278
- Define Resolvers using `createResolver` method:
279
-
280
- ```typescript
281
- createResolver({
282
- name: "add",
283
- operation: "query",
284
- input: {
285
- left: t.int(),
286
- right: t.int(),
287
- },
288
- body: (context) => {
289
- return {
290
- result: context.input.left + context.input.right,
291
- };
292
- },
293
- output: t.object({
294
- result: t.int(),
295
- }),
296
- });
297
- ```
298
-
299
- #### Input/Output Schemas
300
-
301
- Define Input/Output schemas using methods of `t` object. Basic usage and supported field types are the same as TailorDB. TailorDB-specific options (e.g., index, relation) are not supported.
302
-
303
- You can reuse fields defined with `db` object, but note that unsupported options will be ignored:
304
-
305
- ```typescript
306
- const user = db.type("User", {
307
- name: db.string().unique(),
308
- age: db.int(),
309
- });
310
-
311
- createResolver({
312
- input: {
313
- name: user.fields.name,
314
- },
315
- });
316
- ```
317
-
318
- #### Body Function
319
-
320
- Define actual resolver logic in the `body` function. Function arguments include: `input` (input data), `user` (user performing the operation).
321
-
322
- If you're generating Kysely types with a generator, you can use `getDB` to execute typed queries:
323
-
324
- ```typescript
325
- import { getDB } from "../generated/tailordb";
326
-
327
- createResolver({
328
- name: "getUser",
329
- operation: "query",
330
- input: {
331
- name: t.string(),
332
- },
333
- body: async (context) => {
334
- const db = getDB("tailordb");
335
- const query = db
336
- .selectFrom("User")
337
- .select("id")
338
- .where("name", "=", context.input.name)
339
- .limit(1)
340
- .executeTakeFirstOrThrow();
341
- return {
342
- result: result.id,
343
- };
344
- },
345
- output: t.object({
346
- result: t.uuid(),
347
- }),
348
- });
349
- ```
350
-
351
- ### Executor Patterns
352
-
353
- Define Executors in files matching glob patterns specified in `tailor.config.ts`.
354
-
355
- ```typescript
356
- createExecutor({
357
- name: "user-welcome",
358
- description: "Send welcome email to new users",
359
- trigger: recordCreatedTrigger({
360
- type: user,
361
- condition: ({ newRecord }) => !!newRecord.email && newRecord.isActive,
362
- }),
363
- operation: {
364
- kind: "function",
365
- body: async ({ newRecord }) => {
366
- // Send welcome email logic here
367
- },
368
- },
369
- });
370
- ```
371
-
372
- #### Trigger Types
373
-
374
- **Record Triggers** - Fire when records are created, updated, or deleted:
375
-
376
- - `recordCreatedTrigger()`: Fires when a new record is created
377
- - `recordUpdatedTrigger()`: Fires when a record is updated
378
- - `recordDeletedTrigger()`: Fires when a record is deleted
379
-
380
- Each trigger can include an optional filter function:
381
-
382
- ```typescript
383
- recordUpdatedTrigger({
384
- type: order,
385
- condition: ({ newRecord, oldRecord }) =>
386
- newRecord.status === "completed" && oldRecord.status !== "completed",
387
- });
388
- ```
389
-
390
- **Schedule Trigger** - Fires on a cron schedule:
391
-
392
- ```typescript
393
- scheduleTrigger({ cron: "*/5 * * * *" });
394
- scheduleTrigger({ cron: "0 9 * * 1" });
395
- scheduleTrigger({ cron: "0 0 1 * *" });
396
- scheduleTrigger({ cron: "0 * * * *", timezone: "Asia/Tokyo" });
397
- ```
398
-
399
- **Incoming Webhook Trigger** - Fires when an external webhook is received:
400
-
401
- ```typescript
402
- incomingWebhookTrigger<WebhookPayload>();
403
- ```
404
-
405
- **Resolver Executed Trigger** - Fires when a resolver is executed:
406
-
407
- ```typescript
408
- resolverExecutedTrigger({
409
- resolver: createOrderResolver,
410
- condition: ({ result, error }) => !error && result?.order?.id,
411
- }
412
- ```
413
-
414
- #### Execution Targets
415
-
416
- **executeFunction / executeJobFunction** - Execute JavaScript/TypeScript functions:
417
-
418
- ```typescript
419
- createExecutor({
420
- operation: {
421
- kind: "function",
422
- body: async ({ newRecord }) => {
423
- console.log("New record created:", newRecord);
424
- },
425
- },
426
- });
427
- ```
428
-
429
- **executeWebhook** - Call external webhooks with dynamic data:
430
-
431
- ```typescript
432
- createExecutor({
433
- operation: {
434
- kind: "webhook",
435
- url: ({ newRecord }) =>
436
- `https://api.example.com/webhooks/${newRecord.type}`,
437
- headers: {
438
- "Content-Type": "application/json",
439
- "X-API-Key": { vault: "api-keys", key: "external-api" },
440
- },
441
- requestBody: ({ newRecord }) => ({
442
- id: newRecord.id,
443
- timestamp: new Date(),
444
- data: newRecord,
445
- }),
446
- },
447
- });
448
- ```
449
-
450
- **executeGql** - Execute GraphQL queries and mutations:
451
-
452
- ```typescript
453
- createExecutor({
454
- operation: {
455
- kind: "graphql",
456
- appName: "my-app",
457
- query: gql`
458
- mutation UpdateUserStatus($id: ID!, $status: String!) {
459
- updateUser(id: $id, input: { status: $status }) {
460
- id
461
- status
462
- updatedAt
463
- }
464
- }
465
- `,
466
- variables: ({ newRecord }) => ({
467
- id: newRecord.userId,
468
- status: "active",
469
- }),
470
- },
471
- });
472
- ```
473
-
474
- #### Execution Context
475
-
476
- Context varies based on trigger type:
477
-
478
- **Record trigger context** - For `recordCreatedTrigger`, `recordUpdatedTrigger`, and `recordDeletedTrigger`:
479
-
480
- - `typeName`: Name of the TailorDB type
481
- - `newRecord`: New record state (not available for delete triggers)
482
- - `oldRecord`: Previous record state (not available for create triggers)
483
-
484
- **Incoming webhook trigger context** - For `incomingWebhookTrigger`:
485
-
486
- - `body`: Webhook request body
487
- - `headers`: Webhook request headers
488
- - `method`: HTTP method
489
- - `rawBody`: Raw request body as string
490
-
491
- **Resolver executed trigger context** - For `resolverExecutedTrigger`:
492
-
493
- - `resolverName`: Name of the resolver
494
- - `result`: Resolver's return value (when execution succeeds)
495
- - `error`: Error object (when execution fails)
496
-
497
- ### Workflow Concepts
498
-
499
- Define Workflows in files matching glob patterns specified in `tailor.config.ts`. Workflows orchestrate multiple jobs that can depend on each other.
500
-
501
- #### Workflow Rules
502
-
503
- **Important**: All workflow components must follow these rules:
504
-
505
- | Rule | Description |
506
- | ---------------------------------------------- | --------------------------------------------------- |
507
- | `createWorkflow` result must be default export | Workflow files must export the workflow as default |
508
- | All jobs must be named exports | Every job used in a workflow must be a named export |
509
- | Job names must be unique | Job names must be unique across the entire project |
510
- | `mainJob` is required | Every workflow must specify a `mainJob` |
511
- | Jobs in `deps` must be job objects | Pass job objects, not strings |
512
-
513
- #### Workflow Job Definition
514
-
515
- Define workflow jobs using `createWorkflowJob`:
516
-
517
- ```typescript
518
- import { createWorkflowJob } from "@tailor-platform/sdk";
519
- import { getDB } from "../generated/tailordb";
520
-
521
- // All jobs must be named exports
522
- export const fetchCustomer = createWorkflowJob({
523
- name: "fetch-customer",
524
- body: async (input: { customerId: string }) => {
525
- const db = getDB("tailordb");
526
- const customer = await db
527
- .selectFrom("Customer")
528
- .selectAll()
529
- .where("id", "=", input.customerId)
530
- .executeTakeFirst();
531
- return customer;
532
- },
533
- });
534
- ```
535
-
536
- #### Job Dependencies
537
-
538
- Jobs can depend on other jobs using the `deps` array. Dependent jobs are accessible via the second argument of `body` function with hyphens replaced by underscores:
539
-
540
- ```typescript
541
- import { createWorkflowJob } from "@tailor-platform/sdk";
542
- import { fetchCustomer } from "./jobs/fetch-customer";
543
- import { sendNotification } from "./jobs/send-notification";
544
-
545
- // All jobs must be named exports - including jobs with dependencies
546
- export const processOrder = createWorkflowJob({
547
- name: "process-order",
548
- deps: [fetchCustomer, sendNotification],
549
- body: async (input: { orderId: string; customerId: string }, { jobs }) => {
550
- // Access dependent jobs with hyphens replaced by underscores
551
- // "fetch-customer" -> jobs.fetch_customer()
552
- // "send-notification" -> jobs.send_notification()
553
- const customer = await jobs.fetch_customer({
554
- customerId: input.customerId,
555
- });
556
-
557
- const notification = await jobs.send_notification({
558
- message: `Order ${input.orderId} is being processed`,
559
- recipient: customer.email,
560
- });
561
-
562
- return {
563
- orderId: input.orderId,
564
- customerEmail: customer.email,
565
- notificationSent: notification.sent,
566
- };
567
- },
568
- });
569
- ```
570
-
571
- #### Workflow Definition
572
-
573
- Define a workflow using `createWorkflow` and export it as default:
574
-
575
- ```typescript
576
- import { createWorkflow, createWorkflowJob } from "@tailor-platform/sdk";
577
- import { fetchCustomer } from "./jobs/fetch-customer";
578
- import { sendNotification } from "./jobs/send-notification";
579
-
580
- // Jobs must be named exports
581
- export const processOrder = createWorkflowJob({
582
- name: "process-order",
583
- deps: [fetchCustomer, sendNotification],
584
- body: async (input, { jobs }) => {
585
- // ... job logic
586
- },
587
- });
588
-
589
- // Workflow must be default export
590
- export default createWorkflow({
591
- name: "order-processing",
592
- mainJob: processOrder,
593
- });
594
- ```
595
-
596
- #### File Organization
597
-
598
- Recommended file structure for workflows:
599
-
600
- ```
601
- workflows/
602
- ├── jobs/
603
- │ ├── fetch-customer.ts # export const fetchCustomer = createWorkflowJob(...)
604
- │ └── send-notification.ts # export const sendNotification = createWorkflowJob(...)
605
- └── order-processing.ts # export const processOrder = createWorkflowJob(...)
606
- # export default createWorkflow(...)
607
- ```
608
-
609
- All jobs can be in a single file or split across multiple files, as long as they are named exports.