From f7366982cebbd83a8398d53caa0f7a965a6a36db Mon Sep 17 00:00:00 2001 From: Anukiran Date: Wed, 1 Apr 2026 14:57:53 -0500 Subject: [PATCH 1/2] docs update tutorials --- docs/guides/tailordb/versioning.md | 10 +- .../manage-data-schema/create-data-schema.md | 4 +- docs/tutorials/resolver.md | 57 +++++- .../setup-auth/register-identity-provider.md | 165 +++++++++--------- .../setup-executor/event-based-trigger.md | 25 ++- 5 files changed, 157 insertions(+), 104 deletions(-) diff --git a/docs/guides/tailordb/versioning.md b/docs/guides/tailordb/versioning.md index a7c9e1e..5672bf4 100644 --- a/docs/guides/tailordb/versioning.md +++ b/docs/guides/tailordb/versioning.md @@ -11,7 +11,7 @@ This approach preserves historical data and enables change monitoring and analys ## How to enable Data Versioning 1. Create a history table -2. Enable `PublishRecordEvents` settings +2. Enable `publishEvents` feature 3. Add an event based trigger ## Example @@ -51,15 +51,15 @@ db.type("StockSummaryHistory", { }); ``` -### 2. Enable `PublishRecordEvents` settings +### 2. Enable `publishEvents` feature -By enabling `PublishRecordEvents` in the `StockSummary` settings, you can create an event-based trigger that executes on every `StockSummary` record update. +By enabling `publishEvents` in the `StockSummary` features, you can create an event-based trigger that executes on every `StockSummary` record update. ```typescript db.type("StockSummary", { // fields... -}).settings({ - publishRecordEvents: true, +}).features({ + publishEvents: true, }); ``` diff --git a/docs/tutorials/manage-data-schema/create-data-schema.md b/docs/tutorials/manage-data-schema/create-data-schema.md index 9d7462a..c1e6115 100644 --- a/docs/tutorials/manage-data-schema/create-data-schema.md +++ b/docs/tutorials/manage-data-schema/create-data-schema.md @@ -30,7 +30,7 @@ export const task = db.type("Task", { .enum(["todo", "in_progress", "completed", "blocked"]) .description("Current task status"), priority: db.enum(["low", "medium", "high", "urgent"]).description("Task priority level"), - projectId: db.string().foreignKey(project).description("Associated project"), + projectId: db.uuid().relation({ type: "n-1", toward: { type: project } }).description("Associated project"), assigneeId: db.string().optional().description("ID of assigned team member"), dueDate: db.string().optional().description("Task due date"), estimatedHours: db.float().optional().description("Estimated hours to complete"), @@ -45,7 +45,7 @@ export type task = typeof task; - **description**: Optional detailed description - **status**: Enumeration with predefined status values - **priority**: Enumeration for priority levels -- **projectId**: Foreign key relationship to the Project type +- **projectId**: Many-to-one relation to the Project type - **assigneeId**: Optional reference to a team member - **dueDate**: Optional date field - **estimatedHours**: Optional float for time estimation diff --git a/docs/tutorials/resolver.md b/docs/tutorials/resolver.md index 34bccf5..190b464 100644 --- a/docs/tutorials/resolver.md +++ b/docs/tutorials/resolver.md @@ -49,8 +49,8 @@ export const task = db.type("Task", { description: db.string().optional().description("Task description"), status: db.enum(["todo", "in_progress", "completed"]).description("Task status"), priority: db.enum(["low", "medium", "high"]).description("Task priority"), - projectId: db.string().foreignKey(project).description("Associated project"), - assigneeId: db.string().foreignKey(teamMember).optional().description("Assigned team member"), + projectId: db.uuid().relation({ type: "n-1", toward: { type: project } }).description("Associated project"), + assigneeId: db.uuid().relation({ type: "n-1", toward: { type: teamMember } }).optional().description("Assigned team member"), dueDate: db.string().optional().description("Due date"), ...db.fields.timestamps(), }); @@ -153,7 +153,15 @@ export default createResolver({ ## 4. Deploy and Test -Deploy your changes to the workspace: +First, generate Kysely types for type-safe database queries: + +```bash +npm run generate +``` + +This generates TypeScript types and the `getDB()` helper in the `generated/` directory based on your TailorDB schema. + +Then deploy your changes to the workspace: ```bash npm run deploy -- --workspace-id @@ -248,6 +256,49 @@ You can view resolver execution logs in the [Console](https://console.tailor.tec This helps you debug issues and understand how your resolver processes requests. +## Advanced: Triggering Executors from Resolvers + +You can configure resolvers to publish events when they execute, allowing executors to trigger automatically after resolver execution. This is useful for post-processing tasks like sending notifications or updating related data. + +```typescript +export default createResolver({ + name: "assignTask", + operation: "mutation", + publishEvents: true, // Enable event publishing + // ... rest of resolver config +}); +``` + +**How it works:** + +- When `publishEvents: true`, the resolver publishes execution events +- Executors can listen for these events using `resolverExecutedTrigger()` +- The SDK **automatically enables** `publishEvents` when an executor references the resolver + +**Example executor that triggers after resolver execution:** + +```typescript +import { createExecutor, resolverExecutedTrigger } from "@tailor-platform/sdk"; +import assignTaskResolver from "../resolver/assign-task"; + +export default createExecutor({ + name: "notify-task-assigned", + trigger: resolverExecutedTrigger({ + resolver: assignTaskResolver, + condition: ({ result, error }) => !error && !!result.taskId, + }), + operation: { + kind: "function", + body: async ({ result }) => { + console.log(`Task ${result.taskId} assigned to ${result.assigneeName}`); + // Send notification logic here + }, + }, +}); +``` + +For more details, see [Executor Service - Resolver Executed Trigger](../sdk/services/executor#resolver-executed-trigger). + ## Next Steps Learn more about resolvers: diff --git a/docs/tutorials/setup-auth/register-identity-provider.md b/docs/tutorials/setup-auth/register-identity-provider.md index 6c6b994..21d2a4d 100644 --- a/docs/tutorials/setup-auth/register-identity-provider.md +++ b/docs/tutorials/setup-auth/register-identity-provider.md @@ -17,10 +17,45 @@ To enable authentication through an identity provider, you need to register it w ### OIDC (OpenID Connect) -Update your `tailor.config.ts` to include the Auth service with OIDC configuration: +First, ensure you have a User type defined in your database schema (e.g., `db/user.ts`): ```typescript -import { defineConfig, t } from "@tailor-platform/sdk"; +import { db } from "@tailor-platform/sdk"; + +export const user = db.type("User", { + email: db.string().unique(), // usernameField must be unique + name: db.string(), + roles: db.array(db.string()).optional(), + ...db.fields.timestamps(), +}); +``` + +Then update your `tailor.config.ts` to include the Auth service with OIDC configuration: + +```typescript +import { defineAuth, defineConfig } from "@tailor-platform/sdk"; +import { user } from "./db/user"; + +const auth = defineAuth("project-management-auth", { + userProfile: { + type: user, + usernameField: "email", + attributes: { + roles: true, + }, + }, + idProvider: { + name: "oidc-provider", + oidc: { + clientId: process.env.OIDC_CLIENT_ID!, + clientSecret: { + vaultName: "my-vault", + secretName: "oidc-client-secret", + }, + providerUrl: process.env.OIDC_PROVIDER_URL!, + }, + }, +}); export default defineConfig({ name: "project-management", @@ -29,30 +64,7 @@ export default defineConfig({ files: ["db/**/*.ts"], }, }, - auth: { - namespace: "project-management-auth", - idpConfigs: [ - { - name: "oidc-provider", - oidc: { - clientId: process.env.OIDC_CLIENT_ID!, - clientSecret: { - vaultName: "my-vault", - secretName: "oidc-client-secret", - }, - providerUrl: process.env.OIDC_PROVIDER_URL!, - }, - }, - ], - userProfileConfig: { - tailordb: { - namespace: "main-db", - type: "User", - usernameField: "email", - attributeFields: ["roles"], - }, - }, - }, + auth, }); ``` @@ -82,21 +94,6 @@ tailor-sdk secret create \ **Vault naming rules:** Only lowercase letters (a-z), numbers (0-9), and hyphens (-). Must start and end with a letter or number, 2-62 characters long. -**User Type Definition:** - -Make sure you have a User type defined in your database schema (e.g., `db/user.ts`): - -```typescript -import { t } from "@tailor-platform/sdk"; - -export const User = t.object({ - id: t.uuid(), - email: t.string().email(), - name: t.string(), - roles: t.array(t.string()).optional(), -}); -``` - ### SAML The Tailor Platform provides a built-in key for signing SAML authentication requests. When request signing is enabled, the platform automatically signs requests sent from the SP to the IdP. The SP metadata, including the public key for signature verification, is available at `https://api.tailor.tech/saml/{workspace_id}/{auth_namespace}/metadata.xml`. @@ -104,7 +101,27 @@ The Tailor Platform provides a built-in key for signing SAML authentication requ Update your `tailor.config.ts` to include SAML configuration: ```typescript -import { defineConfig } from "@tailor-platform/sdk"; +import { defineAuth, defineConfig } from "@tailor-platform/sdk"; +import { user } from "./db/user"; + +const auth = defineAuth("project-management-auth", { + userProfile: { + type: user, + usernameField: "email", + attributes: { + roles: true, + }, + }, + idProvider: { + name: "saml-provider", + saml: { + metadataUrl: process.env.SAML_METADATA_URL!, + // Alternative: use rawMetadata for inline XML + // rawMetadata: `...`, + enableSignRequest: false, // Set to true to enable request signing + }, + }, +}); export default defineConfig({ name: "project-management", @@ -113,28 +130,7 @@ export default defineConfig({ files: ["db/**/*.ts"], }, }, - auth: { - namespace: "project-management-auth", - idpConfigs: [ - { - name: "saml-provider", - saml: { - metadataUrl: process.env.SAML_METADATA_URL!, - // Alternative: use rawMetadata for inline XML - // rawMetadata: `...`, - enableSignRequest: false, // Set to true to enable request signing - }, - }, - ], - userProfileConfig: { - tailordb: { - namespace: "main-db", - type: "User", - usernameField: "email", - attributeFields: ["roles"], - }, - }, - }, + auth, }); ``` @@ -161,7 +157,25 @@ The metadata URL is provided by your Identity Provider (IdP). You can typically For ID Token-based authentication, update your `tailor.config.ts`: ```typescript -import { defineConfig } from "@tailor-platform/sdk"; +import { defineAuth, defineConfig } from "@tailor-platform/sdk"; +import { user } from "./db/user"; + +const auth = defineAuth("project-management-auth", { + userProfile: { + type: user, + usernameField: "email", + attributes: { + roles: true, + }, + }, + idProvider: { + name: "idtoken-provider", + idToken: { + clientId: process.env.ID_TOKEN_CLIENT_ID!, + providerUrl: process.env.ID_TOKEN_PROVIDER_URL!, + }, + }, +}); export default defineConfig({ name: "project-management", @@ -170,26 +184,7 @@ export default defineConfig({ files: ["db/**/*.ts"], }, }, - auth: { - namespace: "project-management-auth", - idpConfigs: [ - { - name: "idtoken-provider", - idToken: { - clientId: process.env.ID_TOKEN_CLIENT_ID!, - providerUrl: process.env.ID_TOKEN_PROVIDER_URL!, - }, - }, - ], - userProfileConfig: { - tailordb: { - namespace: "main-db", - type: "User", - usernameField: "email", - attributeFields: ["roles"], - }, - }, - }, + auth, }); ``` diff --git a/docs/tutorials/setup-executor/event-based-trigger.md b/docs/tutorials/setup-executor/event-based-trigger.md index 342c7d6..de333fb 100644 --- a/docs/tutorials/setup-executor/event-based-trigger.md +++ b/docs/tutorials/setup-executor/event-based-trigger.md @@ -10,7 +10,7 @@ To create an event-based trigger, you'll need to: 1. Configure the Executor service 2. Create the executor with a record updated trigger -3. Enable `PublishRecordEvents` setting on the Project type +3. Enable `publishEvents` feature on the Project type 4. Deploy the changes 5. Verify the trigger @@ -101,9 +101,9 @@ export default createExecutor({ 3. **Database Query**: Uses Kysely to query task completion statistics before sending the notification -### 3. Enable `PublishRecordEvents` Setting +### 3. Enable `publishEvents` Feature -To ensure the executor triggers when Project records are updated, enable `PublishRecordEvents` in your Project type definition. +To ensure the executor triggers when Project records are updated, enable `publishEvents` in your Project type definition. Update your `db/project.ts` file: @@ -122,12 +122,12 @@ export const project = db createdAt: db.string().description("Creation timestamp"), updatedAt: db.string().description("Last update timestamp"), }) - .settings({ - publishRecordEvents: true, + .features({ + publishEvents: true, }); ``` -The `.settings({ publishRecordEvents: true })` configuration enables the platform to publish events when Project records are created, updated, or deleted. Without this setting, the executor trigger will not fire. +The `.features({ publishEvents: true })` configuration enables the platform to publish events when Project records are created, updated, or deleted. Without this feature enabled, the executor trigger will not fire. ### 4. Deploy the Changes @@ -135,7 +135,14 @@ Before deploying, make sure you have: 1. Created a Slack webhook URL (see [Slack Incoming Webhooks](https://api.slack.com/messaging/webhooks)) 2. Replaced `YOUR_WEBHOOK_URL` in the executor code with your actual webhook URL -3. Generated Kysely types: `npm run generate` (if using Kysely type generator) + +Generate Kysely types for type-safe database access: + +```bash +npm run generate +``` + +This generates TypeScript types and the `getDB()` helper in the `generated/` directory. Deploy your application: @@ -143,7 +150,7 @@ Deploy your application: npm run deploy -- --workspace-id ``` -The SDK will deploy both the updated Project type with `publishRecordEvents` enabled and the new executor. +The SDK will deploy both the updated Project type with `publishEvents` enabled and the new executor. ### 5. Verify the Trigger @@ -212,7 +219,7 @@ Clicking `View Attempts` displays details of job execution attempts. The Tailor **Troubleshooting:** - **No notification received**: Verify your Slack webhook URL is correct -- **Executor not triggering**: Ensure `publishRecordEvents: true` is set on the Project type +- **Executor not triggering**: Ensure `publishEvents: true` is set in `.features()` on the Project type - **Error in logs**: Check the executor logs in the Console for detailed error messages ## Next Steps From a9c98c265ad7b851fc006308d1053bbe3d39a597 Mon Sep 17 00:00:00 2001 From: Anukiran Date: Tue, 7 Apr 2026 18:27:44 -0500 Subject: [PATCH 2/2] update auth and executor tutorials --- .vitepress/config/constants.ts | 7 + docs/tutorials/setup-auth/overview.md | 1 + .../setup-auth/setup-auth-connections.md | 183 ++++++++++++++++++ .../setup-executor/event-based-trigger.md | 54 ++++++ 4 files changed, 245 insertions(+) create mode 100644 docs/tutorials/setup-auth/setup-auth-connections.md diff --git a/.vitepress/config/constants.ts b/.vitepress/config/constants.ts index c8a4cc9..5d2852f 100644 --- a/.vitepress/config/constants.ts +++ b/.vitepress/config/constants.ts @@ -85,6 +85,13 @@ export const defaultSidebarOrder: string[] = ["overview", "quickstart"]; // Section-specific overrides (only if you need different ordering for a specific section) export const sidebarItemOrder: Record = { + "setup-auth": [ + "overview", + "setup-identity-provider", + "register-identity-provider", + "setup-auth-connections", + "login", + ], function: ["overview", "builtin-interfaces"], "app-shell": [ "introduction", diff --git a/docs/tutorials/setup-auth/overview.md b/docs/tutorials/setup-auth/overview.md index 241dd28..19eff25 100644 --- a/docs/tutorials/setup-auth/overview.md +++ b/docs/tutorials/setup-auth/overview.md @@ -26,3 +26,4 @@ The second approach is to request an ID token from your identity provider, then - [Create users](login/create-user) - [Create an OAuth2 client](login/create-oauth2-client) - [Log in using ID token](login/id-token) +- [Set up Auth Connections](setup-auth-connections) diff --git a/docs/tutorials/setup-auth/setup-auth-connections.md b/docs/tutorials/setup-auth/setup-auth-connections.md new file mode 100644 index 0000000..d31f67a --- /dev/null +++ b/docs/tutorials/setup-auth/setup-auth-connections.md @@ -0,0 +1,183 @@ +# Setting up Auth Connections + +Auth connections enable your application to authenticate with external OAuth2 providers (such as Google, Microsoft 365, or QuickBooks) on behalf of itself, not on behalf of a user. Functions, executors, and workflows can then access those provider APIs at runtime using a managed access token. + +- To follow along, first complete the [SDK Quickstart](../../sdk/quickstart) and [Setting up Auth](overview). + +## What you'll build + +A connection to Google's API that your resolvers and functions can use to call Google services without managing OAuth2 tokens manually. + +## Tutorial Steps + +1. Configure the connection in `defineAuth()` +2. Deploy the connection +3. Authorize the connection +4. Use the connection token at runtime +5. Manage connections via CLI + +### 1. Configure the Connection + +Add a `connections` block to your `defineAuth()` in `tailor.config.ts`: + +```typescript +import { defineAuth } from "@tailor-platform/sdk"; +import { idp } from "./idp"; + +export const auth = defineAuth("my-auth", { + userProfile: { + type: user, + usernameField: "email", + }, + idProvider: idp.provider("my-provider", "my-client"), + connections: { + "google-connection": { + type: "oauth2", + providerUrl: "https://accounts.google.com", + issuerUrl: "https://accounts.google.com", + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }, + }, +}); +``` + +**Connection config fields:** + +| Field | Required | Description | +|---|---|---| +| `type` | Yes | Connection type. Currently only `"oauth2"`. | +| `providerUrl` | Yes | OAuth2 provider base URL. | +| `issuerUrl` | Yes | OAuth2 issuer URL for JWT validation. | +| `clientId` | Yes | Your OAuth2 app's client ID. | +| `clientSecret` | Yes | Your OAuth2 app's client secret. | +| `authUrl` | No | Override the authorization endpoint. | +| `tokenUrl` | No | Override the token endpoint. | + +Store `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in your `.env` file or CI secrets. Never commit them. + +### 2. Deploy the Connection + +Run `tailor-sdk apply` to register the connection with the platform: + +```bash +tailor-sdk apply +``` + +This creates the connection record. The connection exists but is not yet authorized (it has no tokens yet). + +::: info Hash-based change detection +The SDK only re-deploys connections whose config has changed since the last `apply`. Delete `.tailor-sdk/` to force all connections to re-sync. +::: + +### 3. Authorize the Connection + +Run the authorize command to complete the OAuth2 flow: + +```bash +tailor-sdk authconnection authorize --name google-connection \ + --scopes "openid,profile,email" +``` + +This opens a browser tab for the OAuth2 consent screen. After you approve, the platform exchanges the authorization code for tokens and stores them securely. Your app code never handles the tokens directly. + +Verify the connection is authorized: + +```bash +tailor-sdk authconnection list +``` + +### 4. Use the Connection Token at Runtime + +Use `auth.getConnectionToken()` in a resolver, executor, or workflow to retrieve the current access token: + +```typescript +// resolvers/fetch-google-profile.ts +import { createResolver, t } from "@tailor-platform/sdk"; +import { auth } from "../tailor.config"; + +export default createResolver({ + name: "fetchGoogleProfile", + operation: "query", + input: {}, + output: t.object({ + email: t.string(), + name: t.string(), + }), + body: async () => { + const tokens = await auth.getConnectionToken("google-connection"); + + const response = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", { + headers: { + Authorization: `Bearer ${tokens.access_token}`, + }, + }); + + const profile = await response.json(); + return { email: profile.email, name: profile.name }; + }, +}); +``` + +The connection name is **type-checked**. If you rename or remove a connection from `defineAuth()`, TypeScript will flag any call sites immediately. + +```typescript +// auth.getConnectionToken("unknown-connection"); // ❌ TypeScript error +``` + +### 5. Manage Connections via CLI + +```bash +# List all connections and their status +tailor-sdk authconnection list + +# Re-authorize a connection (e.g. token expired or scopes changed) +tailor-sdk authconnection authorize --name google-connection \ + --scopes "openid,profile,email,https://www.googleapis.com/auth/calendar" + +# Revoke a connection +tailor-sdk authconnection revoke --name google-connection +``` + +## Complete Example: Calling an External API from an Executor + +Here's a full executor that calls the Google Calendar API whenever a meeting record is created: + +```typescript +// executor/sync-to-google-calendar.ts +import { createExecutor, recordCreatedTrigger } from "@tailor-platform/sdk"; +import { auth } from "../tailor.config"; +import { meeting } from "../db/meeting"; + +export default createExecutor({ + name: "sync-to-google-calendar", + description: "Create a Google Calendar event when a meeting is scheduled", + trigger: recordCreatedTrigger({ type: meeting }), + operation: { + kind: "function", + body: async ({ newRecord }) => { + const tokens = await auth.getConnectionToken("google-connection"); + + await fetch("https://www.googleapis.com/calendar/v3/calendars/primary/events", { + method: "POST", + headers: { + Authorization: `Bearer ${tokens.access_token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + summary: newRecord.title, + start: { dateTime: newRecord.startTime }, + end: { dateTime: newRecord.endTime }, + }), + }); + }, + }, +}); +``` + +## Next Steps + +- [Auth Service](../../sdk/services/auth#auth-connections) - Full auth connections API reference +- [Built-in Interfaces](/guides/function/builtin-interfaces#auth-connection) - Runtime token API +- [Auth Guide](/guides/auth/authconnection) - Platform-level auth connection docs +- [Setting up Auth](overview) - Back to auth tutorial overview diff --git a/docs/tutorials/setup-executor/event-based-trigger.md b/docs/tutorials/setup-executor/event-based-trigger.md index de333fb..bf71efa 100644 --- a/docs/tutorials/setup-executor/event-based-trigger.md +++ b/docs/tutorials/setup-executor/event-based-trigger.md @@ -222,6 +222,60 @@ Clicking `View Attempts` displays details of job execution attempts. The Tailor - **Executor not triggering**: Ensure `publishEvents: true` is set in `.features()` on the Project type - **Error in logs**: Check the executor logs in the Console for detailed error messages +## Multi-Event Triggers + +Instead of creating separate executors for each event type, you can use `recordTrigger()` to handle multiple events in a single executor. This is useful when the response logic is similar across events. + +Update `executor/notify-project-completion.ts` to also handle project creation: + +```typescript +import { createExecutor, recordTrigger } from "@tailor-platform/sdk"; +import { project } from "../db/project"; + +export default createExecutor({ + name: "notify-project-changes", + description: "Send Slack notification on project create or status change to COMPLETED", + trigger: recordTrigger({ + type: project, + events: ["created", "updated"], + }), + operation: { + kind: "webhook", + url: () => "https://hooks.slack.com/services/YOUR_WEBHOOK_URL", + headers: { + "Content-Type": "application/json", + }, + requestBody: async ({ event, newRecord, oldRecord }) => { + if (event === "created") { + return { + text: `📋 New Project Created: ${newRecord.name}`, + }; + } + + // event === "updated" + if (newRecord.status === "COMPLETED" && oldRecord?.status !== "COMPLETED") { + return { + text: `🎉 Project Completed: ${newRecord.name}`, + }; + } + + return null; // No notification for other updates + }, + }, +}); +``` + +**Key differences from single-event triggers:** + +| | Single-event (`recordUpdatedTrigger`) | Multi-event (`recordTrigger`) | +|---|---|---| +| Import | `recordUpdatedTrigger` | `recordTrigger` | +| Events | Fixed to one event | Pass `events: [...]` array | +| Args | Always has `oldRecord` + `newRecord` | Use `args.event` to narrow the type | +| Use case | Simple, focused handlers | Shared logic across create/update/delete | + +The `event` field on args is typed to match the `events` array you pass, so TypeScript will narrow correctly in `if (event === "created")` branches. + ## Next Steps Learn more about executors: