Skip to content

Context

Context lets you pass per-request data (current user, tenant, locale, time window, etc.) into computed and derived field resolvers. The shape is fully typed via the third generic parameter of createRelayerEntity, so you get autocomplete and type safety inside resolve and query callbacks — no casts.

Defining the context type

Pass your context interface as the third generic to createRelayerEntity. Every computed/derived resolver on the entity then receives a typed context:

import { createRelayerEntity } from '@relayerjs/drizzle';
import * as schema from './schema';
interface AppContext {
currentUserId: number;
tenantId: string;
}
const UserEntity = createRelayerEntity<typeof schema, 'users', AppContext>(schema, 'users');

Using context in computed fields

context inside resolve is typed as AppContext directly — no as cast needed:

class User extends UserEntity {
@UserEntity.computed({
resolve: ({ table, sql, context }) =>
sql`CASE WHEN ${table.id} = ${context.currentUserId} THEN true ELSE false END`,
})
isMe!: boolean;
}

Using context in derived fields

The query callback receives the same typed context:

class User extends UserEntity {
@UserEntity.derived({
query: ({ db, schema: s, sql, context, field }) =>
db
.select({ [field()]: sql`count(*)::int`, userId: s.orders.userId })
.from(s.orders)
.where(sql`${s.orders.tenantId} = ${context.tenantId}`)
.groupBy(s.orders.userId),
on: ({ parent, derived, eq }) => eq(parent.id, derived.userId),
})
tenantOrderCount!: number;
}

Passing context per-query

Provide context values in each query call. The shape is checked against the entity’s TContext:

const users = await r.users.findMany({
select: { id: true, firstName: true, isMe: true },
context: { currentUserId: 42, tenantId: 'acme' },
});
// [
// { id: 42, firstName: 'John', isMe: true },
// { id: 43, firstName: 'Jane', isMe: false },
// ]

If you forget a field or pass the wrong shape, TypeScript will reject the call at compile time.

Typical use cases

Current user

interface AppContext {
userId: number;
}
const PostEntity = createRelayerEntity<typeof schema, 'posts', AppContext>(schema, 'posts');
class Post extends PostEntity {
@PostEntity.computed({
resolve: ({ table, sql, context }) =>
sql`CASE WHEN ${table.createdBy} = ${context.userId} THEN true ELSE false END`,
})
isOwner!: boolean;
}

Multi-tenancy

interface AppContext {
tenantId: string;
}
const UserEntity = createRelayerEntity<typeof schema, 'users', AppContext>(schema, 'users');
class User extends UserEntity {
@UserEntity.derived({
query: ({ db, schema: s, sql, context, field }) =>
db
.select({ [field()]: sql`count(*)::int`, userId: s.orders.userId })
.from(s.orders)
.where(sql`${s.orders.tenantId} = ${context.tenantId}`)
.groupBy(s.orders.userId),
on: ({ parent, derived, eq }) => eq(parent.id, derived.userId),
})
tenantOrderCount!: number;
}

Time-based filtering

interface AppContext {
since: Date;
}
const UserEntity = createRelayerEntity<typeof schema, 'users', AppContext>(schema, 'users');
class User extends UserEntity {
@UserEntity.derived({
query: ({ db, schema: s, sql, context, field }) =>
db
.select({ [field()]: sql`count(*)::int`, userId: s.events.userId })
.from(s.events)
.where(sql`${s.events.createdAt} >= ${context.since}`)
.groupBy(s.events.userId),
on: ({ parent, derived, eq }) => eq(parent.id, derived.userId),
})
recentActivity!: number;
}

Where context flows

Context propagates through every read method on the entity client: findMany, findFirst, count, findManyStream, and aggregate. Mutation methods (create, update, delete, etc.) also accept a context option, so when used through @relayerjs/nestjs-crud it can drive row-level filtering via getDefaultWhere — see NestJS Query Service.