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 Resolver | SDK Resolver |
|---|---|
| Multiple steps with different operations | Single body function |
| Declarative step configuration | Imperative TypeScript code |
| Built-in TailorDB/GraphQL steps | Direct database access via Kysely |
| CEL expressions for data transformation | Native TypeScript transformations |
Example Comparison
Pipeline Resolver (Tailor Platform native):
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):
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
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:
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:
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 validateddata- The entire input objectuser- 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 requestuser- 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:
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:
// 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:
createResolver({
name: "processOrder",
operation: "mutation",
publishEvents: true,
// ...
});Behavior:
- When
publishEvents: true, resolver execution events are published - When not specified, it is automatically set to
trueif an executor uses this resolver withresolverExecutedTrigger - When explicitly set to
falsewhile an executor uses this resolver, an error is thrown duringtailor apply
Use cases:
Auto-detection (recommended): Don't set
publishEvents- the SDK automatically enables it when needed by executorstypescript// 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"), // ... });Manual enable: Enable event publishing for external consumers or debugging
typescriptcreateResolver({ name: "auditAction", operation: "mutation", publishEvents: true, // Enable even without executor triggers // ... });Explicit disable: Disable event publishing for a resolver that doesn't need it (error if executor uses it)
typescriptcreateResolver({ name: "internalHelper", operation: "query", publishEvents: false, // Explicitly disable // ... });