Skip to content

Resolver

Resolvers are custom GraphQL endpoints with business logic that execute on the Tailor Platform.

Overview

Resolvers provide:

  • Custom GraphQL queries and mutations
  • Type-safe input/output schemas
  • Access to TailorDB via Kysely query builder
  • User context for authentication/authorization

Comparison with Tailor Platform Pipeline Resolver

The SDK's Resolver is a simplified version of Tailor Platform's Pipeline Resolver.

Pipeline ResolverSDK Resolver
Multiple steps with different operationsSingle body function
Declarative step configurationImperative TypeScript code
Built-in TailorDB/GraphQL stepsDirect database access via Kysely
CEL expressions for data transformationNative TypeScript transformations

Example Comparison

Pipeline Resolver (Tailor Platform native):

yaml
steps:
  - name: getUser
    operation: tailordb.query
    params:
      type: User
      filter:
        email: { eq: "{{ input.email }}" }
  - name: updateAge
    operation: tailordb.mutation
    params:
      type: User
      id: "{{ steps.getUser.id }}"
      input:
        age: "{{ steps.getUser.age + 1 }}"

Resolver (SDK):

typescript
createResolver({
  name: "incrementUserAge",
  operation: "mutation",
  input: { email: t.string() },
  body: async (context) => {
    const db = getDB("tailordb");
    const user = await db
      .selectFrom("User")
      .selectAll()
      .where("email", "=", context.input.email)
      .executeTakeFirstOrThrow();

    await db
      .updateTable("User")
      .set({ age: user.age + 1 })
      .where("id", "=", user.id)
      .execute();

    return { oldAge: user.age, newAge: user.age + 1 };
  },
  output: t.object({ oldAge: t.int(), newAge: t.int() }),
});

Creating a Resolver

Define resolvers in files matching glob patterns specified in tailor.config.ts.

Definition Rules:

  • One resolver per file: Each file must contain exactly one resolver definition
  • Export method: Must use export default
  • Uniqueness: Resolver names must be unique per namespace
typescript
import { createResolver, t } from "@tailor-platform/sdk";

export default createResolver({
  name: "add",
  operation: "query",
  input: {
    left: t.int(),
    right: t.int(),
  },
  body: (context) => {
    return {
      result: context.input.left + context.input.right,
    };
  },
  output: t.object({
    result: t.int(),
  }),
});

Input/Output Schemas

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.

You can reuse fields defined with db object, but note that unsupported options will be ignored:

typescript
const user = db.type("User", {
  name: db.string().unique(),
  age: db.int(),
});

createResolver({
  input: {
    name: user.fields.name,
  },
});

Input Validation

Add validation rules to input fields using the validate method:

typescript
createResolver({
  name: "createUser",
  operation: "mutation",
  input: {
    email: t
      .string()
      .validate(
        ({ value }) => value.includes("@"),
        [({ value }) => value.length <= 255, "Email must be 255 characters or less"],
      ),
    age: t.int().validate(({ value }) => value >= 0 && value <= 150),
  },
  body: (context) => {
    // Input is validated before body executes
    return { email: context.input.email };
  },
  output: t.object({ email: t.string() }),
});

Validation functions receive:

  • value - The field value being validated
  • data - The entire input object
  • user - The user performing the operation

You can specify validation as:

  • A function returning boolean (uses default error message)
  • A tuple of [function, errorMessage] for custom error messages
  • Multiple validators (pass multiple arguments to validate)

Body Function

Define actual resolver logic in the body function. Function arguments include:

  • input - Input data from GraphQL request
  • user - User performing the operation

Using Kysely for Database Access

If you're generating Kysely types with a generator, you can use getDB to execute typed queries:

typescript
import { getDB } from "../generated/tailordb";

createResolver({
  name: "getUser",
  operation: "query",
  input: {
    name: t.string(),
  },
  body: async (context) => {
    const db = getDB("tailordb");
    const result = await db
      .selectFrom("User")
      .select("id")
      .where("name", "=", context.input.name)
      .limit(1)
      .executeTakeFirstOrThrow();
    return {
      result: result.id,
    };
  },
  output: t.object({
    result: t.uuid(),
  }),
});

Query vs Mutation

Use operation: "query" for read operations and operation: "mutation" for write operations:

typescript
// Query - for reading data
createResolver({
  name: "getUsers",
  operation: "query",
  // ...
});

// Mutation - for creating, updating, or deleting data
createResolver({
  name: "createUser",
  operation: "mutation",
  // ...
});

Event Publishing

Enable event publishing for a resolver to trigger executors on resolver execution:

typescript
createResolver({
  name: "processOrder",
  operation: "mutation",
  publishEvents: true,
  // ...
});

Behavior:

  • 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

Use cases:

  1. Auto-detection (recommended): Don't set publishEvents - the SDK automatically enables it when needed by executors

    typescript
    // publishEvents is automatically enabled because an executor uses this resolver
    export default createResolver({
      name: "processPayment",
      operation: "mutation",
      // publishEvents not set - auto-detected
      // ...
    });
    
    // In executor file:
    export default createExecutor({
      trigger: resolverExecutedTrigger("processPayment"),
      // ...
    });
  2. Manual enable: Enable event publishing for external consumers or debugging

    typescript
    createResolver({
      name: "auditAction",
      operation: "mutation",
      publishEvents: true, // Enable even without executor triggers
      // ...
    });
  3. Explicit disable: Disable event publishing for a resolver that doesn't need it (error if executor uses it)

    typescript
    createResolver({
      name: "internalHelper",
      operation: "query",
      publishEvents: false, // Explicitly disable
      // ...
    });