@tailor-platform/sdk 0.0.1 → 0.8.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.
@@ -0,0 +1,504 @@
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 |
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
+ The `assertNonNull` option creates a field that doesn't allow null in TypeScript types but does allow null in TailorDB. This is useful for fields like createdAt where non-null values are set through hooks:
55
+
56
+ ```typescript
57
+ db.string({ optional: true, assertNonNull: true }).hooks({
58
+ create: () => "created value",
59
+ update: () => "updated value",
60
+ });
61
+ ```
62
+
63
+ #### Field Modifiers
64
+
65
+ **Description** - Add a description to field:
66
+
67
+ ```typescript
68
+ db.string().description("User's full name");
69
+ ```
70
+
71
+ **Index / Unique** - Add an index to field:
72
+
73
+ ```typescript
74
+ db.string().index();
75
+ db.string().unique();
76
+ ```
77
+
78
+ **Relations** - Add a relation to field with automatic index and foreign key constraint:
79
+
80
+ ```typescript
81
+ const role = db.type("Role", {
82
+ name: db.string(),
83
+ });
84
+
85
+ const user = db.type("User", {
86
+ name: db.string(),
87
+ roleId: db.uuid().relation({
88
+ type: "n-1",
89
+ toward: { type: role },
90
+ }),
91
+ });
92
+ ```
93
+
94
+ For one-to-one relations, use `type: "1-1"`:
95
+
96
+ ```typescript
97
+ const userProfile = db.type("UserProfile", {
98
+ userId: db.uuid().relation({
99
+ type: "1-1",
100
+ toward: { type: user },
101
+ }),
102
+ bio: db.string(),
103
+ });
104
+ ```
105
+
106
+ For foreign key constraint without creating a relation, use `type: "keyOnly"`:
107
+
108
+ ```typescript
109
+ const user = db.type("User", {
110
+ roleId: db.uuid().relation({
111
+ type: "keyOnly",
112
+ toward: { type: role },
113
+ }),
114
+ });
115
+ ```
116
+
117
+ Create relations against different fields using `toward.key`:
118
+
119
+ ```typescript
120
+ const user = db.type("User", {
121
+ email: db.string().unique(),
122
+ });
123
+
124
+ const userProfile = db.type("UserProfile", {
125
+ userEmail: db.string().relation({
126
+ type: "1-1",
127
+ toward: { type: user, key: "email" },
128
+ }),
129
+ });
130
+ ```
131
+
132
+ Customize relation names using `toward.as` / `backward` options:
133
+
134
+ ```typescript
135
+ const userProfile = db.type("UserProfile", {
136
+ userId: db.uuid().relation({
137
+ type: "1-1",
138
+ toward: { type: user, as: "base" },
139
+ backward: "profile",
140
+ }),
141
+ });
142
+ ```
143
+
144
+ **Hooks** - Add hooks to execute functions during data creation or update:
145
+
146
+ ```typescript
147
+ db.datetime().hooks({
148
+ create: ({ value }) => new Date().toISOString(),
149
+ update: ({ value }) => new Date().toISOString(),
150
+ });
151
+ ```
152
+
153
+ Function arguments include: `value` (field value), `data` (entire record value), `user` (user performing the operation).
154
+
155
+ **Validation** - Add validation functions to field:
156
+
157
+ ```typescript
158
+ db.string().validate(
159
+ ({ value }) => value.length > 5,
160
+ [
161
+ ({ value }) => value.length < 10,
162
+ "Value must be shorter than 10 characters",
163
+ ],
164
+ );
165
+ ```
166
+
167
+ **Vector Search** - Enable field for vector search:
168
+
169
+ ```typescript
170
+ db.string().vector();
171
+ ```
172
+
173
+ **Serial / Auto-increment** - Enable field auto-increment:
174
+
175
+ ```typescript
176
+ db.int().serial({
177
+ start: 0,
178
+ maxValue: 100,
179
+ });
180
+
181
+ db.string().serial({
182
+ start: 0,
183
+ format: "CUST_%d",
184
+ });
185
+ ```
186
+
187
+ **Common Fields** - Add common fields using built-in helpers:
188
+
189
+ ```typescript
190
+ export const user = db.type("User", {
191
+ name: db.string(),
192
+ ...db.fields.timestamps(),
193
+ });
194
+ ```
195
+
196
+ #### Type Definition
197
+
198
+ Define a TailorDB Type using `db.type()` method:
199
+
200
+ ```typescript
201
+ db.type("User", {
202
+ name: db.string(),
203
+ });
204
+ ```
205
+
206
+ Specify PluralForm by passing an array as first argument:
207
+
208
+ ```typescript
209
+ db.type(["User", "UserList"], {
210
+ name: db.string(),
211
+ });
212
+ ```
213
+
214
+ Pass a description as second argument:
215
+
216
+ ```typescript
217
+ db.type("User", "User in the system", {
218
+ name: db.string(),
219
+ description: db.string().optional(),
220
+ });
221
+ ```
222
+
223
+ **Composite Indexes** - Configure composite indexes:
224
+
225
+ ```typescript
226
+ db.type("User", {
227
+ firstName: db.string(),
228
+ lastName: db.string(),
229
+ }).indexes({
230
+ fields: ["firstName", "lastName"],
231
+ unique: true,
232
+ name: "user_name_idx",
233
+ });
234
+ ```
235
+
236
+ **File Fields** - Add file fields:
237
+
238
+ ```typescript
239
+ db.type("User", {
240
+ name: db.string(),
241
+ }).files({
242
+ avatar: "profile image",
243
+ });
244
+ ```
245
+
246
+ **Features** - Enable additional features:
247
+
248
+ ```typescript
249
+ db.type("User", {
250
+ name: db.string(),
251
+ }).features({
252
+ aggregation: true,
253
+ bulkUpsert: true,
254
+ });
255
+ ```
256
+
257
+ **Permissions** - Configure Permission and GQLPermission. For details, see the [TailorDB Permission documentation](https://docs.tailor.tech/guides/tailordb/permission).
258
+
259
+ ```typescript
260
+ db.type("User", {
261
+ name: db.string(),
262
+ role: db.enum("admin", "user").index(),
263
+ })
264
+ .permission({
265
+ create: [[{ user: "role" }, "=", "admin"]],
266
+ read: [
267
+ [{ user: "role" }, "=", "admin"],
268
+ [{ record: "id" }, "=", { user: "id" }],
269
+ ],
270
+ update: [[{ user: "role" }, "=", "admin"]],
271
+ delete: [[{ user: "role" }, "=", "admin"]],
272
+ })
273
+ .gqlPermission([
274
+ { conditions: [[{ user: "role" }, "=", "admin"]], actions: "all" },
275
+ { conditions: [[{ user: "role" }, "=", "user"]], actions: ["read"] },
276
+ ]);
277
+ ```
278
+
279
+ Following the secure-by-default principle, all operations are denied if permissions are not configured.
280
+
281
+ ### Resolver Concepts
282
+
283
+ Define Resolvers in files matching glob patterns specified in `tailor.config.ts`.
284
+
285
+ #### Resolver Creation
286
+
287
+ Define Resolvers using `createResolver` method:
288
+
289
+ ```typescript
290
+ createResolver({
291
+ name: "add",
292
+ operation: "query",
293
+ input: {
294
+ left: t.int(),
295
+ right: t.int(),
296
+ },
297
+ body: (context) => {
298
+ return {
299
+ result: context.input.left + context.input.right,
300
+ };
301
+ },
302
+ output: t.object({
303
+ result: t.int(),
304
+ }),
305
+ });
306
+ ```
307
+
308
+ #### Input/Output Schemas
309
+
310
+ 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.
311
+
312
+ You can reuse fields defined with `db` object, but note that unsupported options will be ignored:
313
+
314
+ ```typescript
315
+ const user = db.type("User", {
316
+ name: db.string().unique(),
317
+ age: db.int(),
318
+ });
319
+
320
+ createResolver({
321
+ input: {
322
+ name: user.fields.name,
323
+ },
324
+ });
325
+ ```
326
+
327
+ #### Body Function
328
+
329
+ Define actual resolver logic in the `body` function. Function arguments include: `input` (input data), `user` (user performing the operation).
330
+
331
+ If you're generating Kysely types with a generator, you can use `getDB` to execute typed queries:
332
+
333
+ ```typescript
334
+ import { getDB } from "../generated/tailordb";
335
+
336
+ createResolver({
337
+ name: "getUser",
338
+ operation: "query",
339
+ input: {
340
+ name: t.string(),
341
+ },
342
+ body: async (context) => {
343
+ const db = getDB("tailordb");
344
+ const query = db
345
+ .selectFrom("User")
346
+ .select("id")
347
+ .where("name", "=", context.input.name)
348
+ .limit(1)
349
+ .executeTakeFirstOrThrow();
350
+ return {
351
+ result: result.id,
352
+ };
353
+ },
354
+ output: t.object({
355
+ result: t.uuid(),
356
+ }),
357
+ });
358
+ ```
359
+
360
+ ### Executor Patterns
361
+
362
+ Define Executors in files matching glob patterns specified in `tailor.config.ts`.
363
+
364
+ ```typescript
365
+ createExecutor({
366
+ name: "user-welcome",
367
+ description: "Send welcome email to new users",
368
+ trigger: recordCreatedTrigger({
369
+ type: user,
370
+ condition: ({ newRecord }) => !!newRecord.email && newRecord.isActive,
371
+ }),
372
+ operation: {
373
+ kind: "function",
374
+ body: async ({ newRecord }) => {
375
+ // Send welcome email logic here
376
+ },
377
+ },
378
+ });
379
+ ```
380
+
381
+ #### Trigger Types
382
+
383
+ **Record Triggers** - Fire when records are created, updated, or deleted:
384
+
385
+ - `recordCreatedTrigger()`: Fires when a new record is created
386
+ - `recordUpdatedTrigger()`: Fires when a record is updated
387
+ - `recordDeletedTrigger()`: Fires when a record is deleted
388
+
389
+ Each trigger can include an optional filter function:
390
+
391
+ ```typescript
392
+ recordUpdatedTrigger({
393
+ type: order,
394
+ condition: ({ newRecord, oldRecord }) =>
395
+ newRecord.status === "completed" && oldRecord.status !== "completed",
396
+ });
397
+ ```
398
+
399
+ **Schedule Trigger** - Fires on a cron schedule:
400
+
401
+ ```typescript
402
+ scheduleTrigger({ cron: "*/5 * * * *" });
403
+ scheduleTrigger({ cron: "0 9 * * 1" });
404
+ scheduleTrigger({ cron: "0 0 1 * *" });
405
+ scheduleTrigger({ cron: "0 * * * *", timezone: "Asia/Tokyo" });
406
+ ```
407
+
408
+ **Incoming Webhook Trigger** - Fires when an external webhook is received:
409
+
410
+ ```typescript
411
+ incomingWebhookTrigger<WebhookPayload>();
412
+ ```
413
+
414
+ **Resolver Executed Trigger** - Fires when a resolver is executed:
415
+
416
+ ```typescript
417
+ resolverExecutedTrigger({
418
+ resolver: createOrderResolver,
419
+ condition: ({ result, error }) => !error && result?.order?.id,
420
+ }
421
+ ```
422
+
423
+ #### Execution Targets
424
+
425
+ **executeFunction / executeJobFunction** - Execute JavaScript/TypeScript functions:
426
+
427
+ ```typescript
428
+ createExecutor({
429
+ operation: {
430
+ kind: "function",
431
+ body: async ({ newRecord }) => {
432
+ console.log("New record created:", newRecord);
433
+ },
434
+ },
435
+ });
436
+ ```
437
+
438
+ **executeWebhook** - Call external webhooks with dynamic data:
439
+
440
+ ```typescript
441
+ createExecutor({
442
+ operation: {
443
+ kind: "webhook",
444
+ url: ({ newRecord }) =>
445
+ `https://api.example.com/webhooks/${newRecord.type}`,
446
+ headers: {
447
+ "Content-Type": "application/json",
448
+ "X-API-Key": { vault: "api-keys", key: "external-api" },
449
+ },
450
+ body: ({ newRecord }) => ({
451
+ id: newRecord.id,
452
+ timestamp: new Date().toISOString(),
453
+ data: newRecord,
454
+ }),
455
+ },
456
+ });
457
+ ```
458
+
459
+ **executeGql** - Execute GraphQL queries and mutations:
460
+
461
+ ```typescript
462
+ createExecutor({
463
+ operation: {
464
+ kind: "graphql",
465
+ appName: "my-app",
466
+ query: gql`
467
+ mutation UpdateUserStatus($id: ID!, $status: String!) {
468
+ updateUser(id: $id, input: { status: $status }) {
469
+ id
470
+ status
471
+ updatedAt
472
+ }
473
+ }
474
+ `,
475
+ variables: ({ newRecord }) => ({
476
+ id: newRecord.userId,
477
+ status: "active",
478
+ }),
479
+ },
480
+ });
481
+ ```
482
+
483
+ #### Execution Context
484
+
485
+ Context varies based on trigger type:
486
+
487
+ **Record trigger context** - For `recordCreatedTrigger`, `recordUpdatedTrigger`, and `recordDeletedTrigger`:
488
+
489
+ - `typeName`: Name of the TailorDB type
490
+ - `newRecord`: New record state (not available for delete triggers)
491
+ - `oldRecord`: Previous record state (not available for create triggers)
492
+
493
+ **Incoming webhook trigger context** - For `incomingWebhookTrigger`:
494
+
495
+ - `body`: Webhook request body
496
+ - `headers`: Webhook request headers
497
+ - `method`: HTTP method
498
+ - `rawBody`: Raw request body as string
499
+
500
+ **Resolver executed trigger context** - For `resolverExecutedTrigger`:
501
+
502
+ - `resolverName`: Name of the resolver
503
+ - `result`: Resolver's return value (when execution succeeds)
504
+ - `error`: Error object (when execution fails)
@@ -0,0 +1,96 @@
1
+ # Tailor Platform SDK
2
+
3
+ Development kit for building type-safe applications on the Tailor Platform using TypeScript.
4
+
5
+ ## Getting Started
6
+
7
+ In this quickstart tutorial, you'll create an app using the Tailor Platform SDK.
8
+ Follow the steps below to get started.
9
+
10
+ ## Prerequisite
11
+
12
+ You'll need a Tailor account to start using the Tailor Platform.
13
+ Contact us [here](https://www.tailor.tech/demo) to get started.
14
+
15
+ ### Install Node.js
16
+
17
+ The SDK requires Node.js 22 or later. Install Node.js via your package manager by following the official Node.js instructions.
18
+
19
+ ### Create an Example App
20
+
21
+ The following command creates a new project with the required configuration files and example code.
22
+
23
+ ```bash
24
+ npm create @tailor-platform/sdk example-app --template hello-world
25
+ ```
26
+
27
+ Before deploying your app, you need to create a workspace:
28
+
29
+ ```bash
30
+ npx tailor-sdk login
31
+ npx tailor-sdk workspace create --name <workspace-name> --region <workspace-region>
32
+ npx tailor-sdk workspace list
33
+
34
+ # OR
35
+ # Create a new workspace using Tailor Platform Console
36
+ # https://console.tailor.tech/
37
+ ```
38
+
39
+ ### Deploy Your App
40
+
41
+ Run the apply command to deploy your project:
42
+
43
+ ```bash
44
+ cd example-app
45
+ npm run deploy -- --workspace-id <your-workspace-id>
46
+ ```
47
+
48
+ You can now open the GraphQL Playground and execute the `hello` query:
49
+
50
+ ```graphql
51
+ query {
52
+ hello(input: { name: "sdk" }) {
53
+ message
54
+ }
55
+ }
56
+ ```
57
+
58
+ ### Hello World Example
59
+
60
+ Here's a simple query resolver from the hello-world template:
61
+
62
+ ```typescript
63
+ import { createResolver, t } from "@tailor-platform/sdk";
64
+
65
+ export default createResolver({
66
+ name: "hello",
67
+ operation: "query",
68
+ input: {
69
+ name: t.string().description("Name to greet"),
70
+ },
71
+ body: (context) => {
72
+ return {
73
+ message: `Hello, ${context.input.name}!`,
74
+ };
75
+ },
76
+ output: t
77
+ .object({
78
+ message: t.string().description("Greeting message"),
79
+ })
80
+ .description("Greeting response"),
81
+ });
82
+ ```
83
+
84
+ You can edit `src/resolvers/hello.ts` to customize the message:
85
+
86
+ ```typescript
87
+ export default createResolver({
88
+ body: (context) => {
89
+ return {
90
+ message: `Goodbye, ${context.input.name}!`,
91
+ };
92
+ },
93
+ });
94
+ ```
95
+
96
+ Deploy again to see the response.