Functions API
defineQuery, defineMutation, context types, and the database API.
// valet/todos.ts
import { defineQuery, defineMutation, v } from './_generated/valet/api'
export const list = defineQuery({
args: { completed: v.optional(v.boolean()) },
execution: 'local',
handler: async (ctx, args) => {
let query = ctx.db.query('todos')
if (args.completed !== undefined) {
query = query.filter((q) => q.eq('completed', args.completed))
}
return query.order('createdAt', 'desc').collect()
},
})
export const create = defineMutation({
args: { title: v.string() },
handler: async (ctx, args) => {
return ctx.db.insert('todos', {
title: args.title,
completed: false,
createdAt: Date.now(),
})
},
})The functions API defines queries and mutations that run against your database. Queries read data; mutations write data. Both receive a context object with a database reader or writer and optional auth information.
defineQuery
// valet/todos.ts
import { defineQuery, v } from './_generated/valet/api'
export const list = defineQuery({
args: { limit: v.optional(v.number()) },
execution: 'local',
handler: async (ctx, args) => {
return ctx.db.query('todos').take(args.limit ?? 50).collect()
},
})
export const search = defineQuery({
args: { query: v.string() },
execution: 'server',
handler: async (ctx, args) => {
return ctx.db.search('articles', 'body', { query: args.query })
},
})Defines a query function. Queries have read-only access to the database.
Signature
function defineQuery<Args, Returns>(definition: QueryDefinition<Args, Returns>): QueryReference<Args, Returns>QueryDefinition
| Field | Type | Required | Description |
|---|---|---|---|
args | FieldDefinitions | Yes | An object of validators defining the query's arguments. Use {} for no arguments. |
execution | QueryExecutionMode | No | Where the query runs. Defaults to 'server'. |
handler | (ctx: QueryContext, args: Args) => Promise<Returns> | Yes | The query handler function. Receives a QueryContext and the validated arguments. |
QueryExecutionMode
| Value | Description |
|---|---|
'local' | Runs against the client-side SQLite replica. Instant, offline-capable, and reactive. |
'server' | Runs on the server. Required for search indexes and server-only tables (sync mode 'none'). |
'fetch' | One-shot server call. No subscription or caching -- the query runs once on the server and returns the result. |
defineMutation
// valet/todos.ts
import { defineMutation, v } from './_generated/valet/api'
export const create = defineMutation({
args: {
title: v.string(),
priority: v.optional(v.number()),
},
handler: async (ctx, args) => {
const id = await ctx.db.insert('todos', {
title: args.title,
completed: false,
priority: args.priority ?? 0,
createdAt: Date.now(),
})
return id
},
})
export const remove = defineMutation({
args: { id: v.id('todos') },
handler: async (ctx, args) => {
await ctx.db.delete(args.id)
},
})Defines a mutation function. Mutations have read-write access to the database. When called from a client, the mutation is written to the local mutation log, applied as an optimistic update, and synced to the server. The server executes the mutation authoritatively and the result is rebased onto the client state.
Signature
function defineMutation<Args, Returns>(definition: MutationDefinition<Args, Returns>): MutationReference<Args, Returns>MutationDefinition
| Field | Type | Required | Description |
|---|---|---|---|
args | FieldDefinitions | Yes | An object of validators defining the mutation's arguments. Use {} for no arguments. |
handler | (ctx: MutationContext, args: Args) => Promise<Returns> | Yes | The mutation handler function. Receives a MutationContext and the validated arguments. |
QueryContext
// valet/todos.ts
import { defineQuery, v } from './_generated/valet/api'
export const myTodos = defineQuery({
args: {},
execution: 'local',
handler: async (ctx) => {
// ctx.db is a DatabaseReader
// ctx.auth contains the authenticated user's info
if (!ctx.auth) throw new Error('Not authenticated')
return ctx.db.query('todos')
.filter((q) => q.eq('userId', ctx.auth!.userId))
.collect()
},
})The context object passed to query handlers.
| Field | Type | Description |
|---|---|---|
db | DatabaseReader | Read-only database access. |
auth | AuthInfo | undefined | The authenticated user's information, if a valid auth token is set. |
MutationContext
// valet/todos.ts
import { defineMutation, v } from './_generated/valet/api'
export const create = defineMutation({
args: { title: v.string() },
handler: async (ctx, args) => {
// ctx.db is a DatabaseWriter (extends DatabaseReader)
// ctx.auth contains the authenticated user's info
if (!ctx.auth) throw new Error('Not authenticated')
return ctx.db.insert('todos', {
title: args.title,
completed: false,
userId: ctx.auth.userId,
})
},
})The context object passed to mutation handlers.
| Field | Type | Description |
|---|---|---|
db | DatabaseWriter | Read-write database access. Extends DatabaseReader. |
auth | AuthInfo | undefined | The authenticated user's information, if a valid auth token is set. |
AuthInfo
// valet/todos.ts
import { defineQuery, v } from './_generated/valet/api'
export const me = defineQuery({
args: {},
execution: 'server',
handler: async (ctx) => {
// AuthInfo fields
const userId: string = ctx.auth!.userId
const claims: Record<string, unknown> | undefined = ctx.auth!.claims
return ctx.db.query('users')
.filter((q) => q.eq('_id', userId))
.unique()
},
})Contains the authenticated user's identity, extracted from the JWT token.
| Field | Type | Description |
|---|---|---|
userId | string | The user's unique identifier (from the JWT sub claim). |
claims | Record<string, unknown> | undefined | Additional claims from the JWT payload. |
DatabaseReader
// valet/todos.ts
import { defineQuery, v } from './_generated/valet/api'
export const getById = defineQuery({
args: { id: v.id('todos') },
execution: 'local',
handler: async (ctx, args) => {
return ctx.db.get(args.id)
},
})
export const list = defineQuery({
args: {},
execution: 'local',
handler: async (ctx) => {
return ctx.db.query('todos').collect()
},
})
export const search = defineQuery({
args: { query: v.string() },
execution: 'server',
handler: async (ctx, args) => {
return ctx.db.search('articles', 'body', {
query: args.query,
limit: 20,
})
},
})Read-only database access. Available in both query and mutation handlers via ctx.db.
.query(table)
Starts a query builder for the specified table. Returns a DatabaseQueryBuilder.
ctx.db.query('todos')| Parameter | Type | Required | Description |
|---|---|---|---|
table | string | Yes | The table name to query. |
Returns: DatabaseQueryBuilder<Doc>
.get(id)
Fetches a single document by its document ID. Returns null if the document does not exist.
const todo = await ctx.db.get(args.id)| Parameter | Type | Required | Description |
|---|---|---|---|
id | Id<TableName> | Yes | The document ID to look up. |
Returns: Promise<Doc | null>
.search(table, field, options)
Runs a full-text search against a search index.
const results = await ctx.db.search('articles', 'body', {
query: 'valet sync',
limit: 10,
})| Parameter | Type | Required | Description |
|---|---|---|---|
table | string | Yes | The table name to search. |
field | string | Yes | The name of the search field (the field indexed with .searchIndex()). |
options | SearchOptions | Yes | Search configuration. |
Returns: Promise<Doc[]>
SearchOptions
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
query | string | Yes | -- | The search query string. |
limit | number | No | -- | Maximum number of results to return. |
DatabaseWriter
// valet/todos.ts
import { defineMutation, v } from './_generated/valet/api'
export const create = defineMutation({
args: { title: v.string() },
handler: async (ctx, args) => {
const id = await ctx.db.insert('todos', {
title: args.title,
completed: false,
})
return id
},
})
export const update = defineMutation({
args: { id: v.id('todos'), title: v.string() },
handler: async (ctx, args) => {
await ctx.db.patch(args.id, { title: args.title })
},
})
export const replace = defineMutation({
args: { id: v.id('todos'), title: v.string(), completed: v.boolean() },
handler: async (ctx, args) => {
await ctx.db.replace(args.id, {
title: args.title,
completed: args.completed,
})
},
})
export const remove = defineMutation({
args: { id: v.id('todos') },
handler: async (ctx, args) => {
await ctx.db.delete(args.id)
},
})Read-write database access. Extends DatabaseReader with write methods. Available in mutation handlers via ctx.db.
.insert(table, document)
Inserts a new document into the table. Returns the generated document ID.
| Parameter | Type | Required | Description |
|---|---|---|---|
table | string | Yes | The table to insert into. |
document | object | Yes | The document fields. Must conform to the table's validators. Do not include _id. |
Returns: Promise<Id<TableName>>
.patch(id, updates)
Partially updates a document. Only the specified fields are changed; all other fields remain unchanged.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | Id<TableName> | Yes | The document ID to update. |
updates | Partial<Doc> | Yes | An object with the fields to update. |
Returns: Promise<void>
.replace(id, document)
Replaces a document entirely. The document's _id remains the same; all other fields are overwritten.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | Id<TableName> | Yes | The document ID to replace. |
document | object | Yes | The complete replacement document. Must conform to the table's validators. Do not include _id. |
Returns: Promise<void>
.delete(id)
Deletes a document by its document ID.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | Id<TableName> | Yes | The document ID to delete. |
Returns: Promise<void>
DatabaseQueryBuilder
// valet/todos.ts
import { defineQuery, v } from './_generated/valet/api'
export const list = defineQuery({
args: {},
execution: 'local',
handler: async (ctx) => {
return ctx.db.query('todos')
.filter((q) => q.eq('completed', false))
.order('createdAt', 'desc')
.take(25)
.collect()
},
})
export const paginated = defineQuery({
args: { cursor: v.optional(v.string()), limit: v.optional(v.number()) },
execution: 'local',
handler: async (ctx, args) => {
return ctx.db.query('todos')
.order('createdAt', 'desc')
.paginate({ limit: args.limit ?? 20, cursor: args.cursor })
},
})
export const first = defineQuery({
args: {},
execution: 'local',
handler: async (ctx) => {
return ctx.db.query('todos')
.filter((q) => q.eq('completed', false))
.order('priority', 'desc')
.first()
},
})
export const byId = defineQuery({
args: { id: v.id('todos') },
execution: 'local',
handler: async (ctx, args) => {
return ctx.db.query('todos')
.filter((q) => q.eq('_id', args.id))
.unique()
},
})The query builder chains methods to filter, order, limit, and collect results. All methods return a new builder (immutable chaining) except the terminal methods (.collect(), .first(), .unique(), .paginate()).
.filter(fn)
Adds a filter predicate. The callback receives a FilterBuilder and returns a filter expression.
.filter((q) => q.eq('completed', false))
.filter((q) => q.and(q.eq('completed', false), q.gt('priority', 3)))| Parameter | Type | Required | Description |
|---|---|---|---|
fn | (q: FilterBuilder) => FilterExpression | Yes | A function that builds the filter expression. |
Returns: DatabaseQueryBuilder (for chaining)
FilterBuilder methods
| Method | Description | Example |
|---|---|---|
q.eq(field, value) | Equal to | q.eq('status', 'active') |
q.neq(field, value) | Not equal to | q.neq('status', 'deleted') |
q.lt(field, value) | Less than | q.lt('priority', 5) |
q.lte(field, value) | Less than or equal | q.lte('priority', 5) |
q.gt(field, value) | Greater than | q.gt('priority', 0) |
q.gte(field, value) | Greater than or equal | q.gte('createdAt', cutoff) |
q.oneOf(field, values) | Value is in the array | q.oneOf('status', ['active', 'pending']) |
q.isNull(field) | Field is null | q.isNull('deletedAt') |
q.isNotNull(field) | Field is not null | q.isNotNull('assignee') |
q.arrayContains(field, value) | Array field contains the value | q.arrayContains('tags', 'urgent') |
q.and(...exprs) | Logical AND | q.and(q.eq('a', 1), q.eq('b', 2)) |
q.or(...exprs) | Logical OR | q.or(q.eq('a', 1), q.eq('a', 2)) |
q.not(expr) | Logical NOT | q.not(q.eq('completed', true)) |
.order(field, direction?)
Sets the sort order for the results.
.order('createdAt', 'desc')
.order('priority') // defaults to 'asc'| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
field | string | Yes | -- | The field to sort by. |
direction | 'asc' | 'desc' | No | 'asc' | Sort direction. |
Returns: DatabaseQueryBuilder (for chaining)
.take(n)
Limits the query to at most n results.
.take(10)| Parameter | Type | Required | Description |
|---|---|---|---|
n | number | Yes | Maximum number of results. |
Returns: DatabaseQueryBuilder (for chaining)
.skip(n)
Skips the first n results.
.skip(20)| Parameter | Type | Required | Description |
|---|---|---|---|
n | number | Yes | Number of results to skip. |
Returns: DatabaseQueryBuilder (for chaining)
.paginate(options)
Returns a paginated result with a cursor for fetching the next page. Terminal method.
const page = await ctx.db.query('todos')
.order('createdAt', 'desc')
.paginate({ limit: 20, cursor: args.cursor })| Parameter | Type | Required | Description |
|---|---|---|---|
options | PaginationOptions | Yes | Pagination configuration. |
Returns: Promise<PaginatedResult<T>>
.collect()
Executes the query and returns all matching documents as an array. Terminal method.
const todos = await ctx.db.query('todos').collect()Returns: Promise<T[]>
.first()
Executes the query and returns the first matching document, or null if no documents match. Terminal method.
const todo = await ctx.db.query('todos')
.filter((q) => q.eq('completed', false))
.first()Returns: Promise<T | null>
.unique()
Executes the query and returns exactly one matching document. Throws if zero or more than one document matches. Terminal method.
const todo = await ctx.db.query('todos')
.filter((q) => q.eq('_id', args.id))
.unique()Returns: Promise<T>
Throws:
'Query on table "X" returned no results, expected exactly one'-- no documents matched.'Query on table "X" returned multiple results, expected exactly one'-- more than one document matched.
PaginationOptions
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
limit | number | Yes | -- | Maximum number of documents per page. |
cursor | string | No | -- | The cursor from a previous PaginatedResult.nextCursor. Omit for the first page. |
PaginatedResult<T>
| Field | Type | Description |
|---|---|---|
data | T[] | The documents in this page. |
nextCursor | string | undefined | The cursor to pass to .paginate() for the next page. undefined if there are no more pages. |
hasMore | boolean | true if there are more pages after this one. |