ContextFilter
The @ContextFilter() decorator allows you to automatically apply filters to all queries and mutations for a specific DTO class. This is useful for implementing row-level security, multi-tenancy, or automatic filtering based on user context.
What You’ll Learn
Section intitulée « What You’ll Learn »This guide will help you understand:
- ✅ How to use the
@ContextFilter()decorator - ✅ How to access context information in filter functions
- ✅ How to apply different filters based on operation type
- ✅ How to return Filter objects or raw SQL strings
- ✅ How to implement multi-tenant filtering
- ✅ Best practices for context filtering
Overview
Section intitulée « Overview »The @ContextFilter() decorator automatically combines with user-provided filters using AND logic. This means:
- The filter from
@ContextFilter()is always applied - User-provided filters are combined with the automatic filter
- Both filters must be satisfied for a record to be returned or modified
Basic Usage
Section intitulée « Basic Usage »Apply a filter function directly to a DTO class:
import { ObjectType, Field } from '@lib/type-graphql';import { ContextFilter, OperationContext } from '@lib/type-graphql-query';
@ObjectType()@ContextFilter<Product>((ctx: OperationContext) => { // Automatically filter to only show products owned by the current user // Access ExecutionContext properties directly from resolverData.context return { userId: { eq: ctx.resolverData.context.userId }, };})export class Product { @Field() id!: string;
@Field() name!: string;
@Field() userId!: string;}OperationContext Type
Section intitulée « OperationContext Type »The OperationContext type provides comprehensive information about the current operation:
Properties
Section intitulée « Properties »resolverData- GraphQL resolver data including the GraphQL contextresolverData.context- The execution context (extendsExecutionContext) with user information, workspace, locale, etc.- You can access
userId,locale,workspaceId, and otherExecutionContextproperties directly fromresolverData.context
operationType- Type of operation:'READ','CREATE','UPDATE','DELETE', or'UPSERT'operationName- Name of the GraphQL operation (e.g.,'products','createOneProduct','updateManyProducts')many- Whether this is an operation for multiple items (true) or a single item (false)
Example: Using Operation Type
Section intitulée « Example: Using Operation Type »You can apply different filters based on the operation type:
import { ObjectType, Field } from '@lib/type-graphql';import { ContextFilter, OperationContext } from '@lib/type-graphql-query';
@ObjectType()@ContextFilter<Product>((ctx: OperationContext) => { const userId = ctx.resolverData.context.userId;
// For read operations, show only user's own products if (ctx.operationType === 'READ') { return { userId: { eq: userId }, }; }
// For mutations, allow access to products the user can modify if (['CREATE', 'UPDATE', 'DELETE', 'UPSERT'].includes(ctx.operationType)) { return { or: [{ userId: { eq: userId } }, { sharedWith: { in: [userId] } }], }; }
return undefined;})export class Product { @Field() id!: string;
@Field() name!: string;
@Field() userId!: string;}TypeScript Type Inference
Section intitulée « TypeScript Type Inference »When using @ContextFilter<DTO>((ctx: OperationContext) => ...), TypeScript will automatically infer the extended ExecutionContext type from module augmentation. This means you can access extended properties like platformRole, workspaceId, etc. without explicit type annotations:
import { ObjectType, Field } from '@lib/type-graphql';import { ContextFilter, OperationContext } from '@lib/type-graphql-query';
@ObjectType()@ContextFilter<Product>((ctx: OperationContext) => { // TypeScript automatically infers the extended ExecutionContext type // Access ExecutionContext properties directly from resolverData.context const userId = ctx.resolverData.context.userId; const workspaceId = ctx.resolverData.context.workspaceId; const platformRole = ctx.resolverData.context.platformRole;
return { userId: { eq: userId }, workspaceId: { eq: workspaceId }, };})export class Product { @Field() id!: string;
@Field() name!: string;}Custom ContextFilter Class
Section intitulée « Custom ContextFilter Class »For more complex filtering logic, you can create a class that implements CustomContextFilter:
import { ObjectType, Field } from '@lib/type-graphql';import { ContextFilter, CustomContextFilter, Operation } from '@lib/type-graphql-query';import type { Filter } from '@lib/type-graphql-query';
class ProductContextFilter implements CustomContextFilter<Product, MyContext> { filter(ctx: OperationContext<MyContext>): Filter<Product> | undefined { const user = ctx.resolverData.context.user;
if (!user) { // No user, return empty result filter return { id: { eq: null } }; // Will return no results }
// Admin users can see all products if (user.roles.includes('admin')) { return undefined; // No filter }
// Regular users can only see their own products return { userId: { eq: user.id }, }; }}
@ObjectType()@ContextFilter(new ProductContextFilter())export class Product { @Field() id!: string;
@Field() name!: string;
@Field() userId!: string;}Return Types
Section intitulée « Return Types »The filter method can return:
Filter<DTO>- A filter object that will be applied to the query/mutationstring- A raw SQL filter string that will be passed asrawFilterto QueryService methods (advanced usage)undefined- No filter will be appliednull- No filter will be applied
Raw SQL Filter (string)
Section intitulée « Raw SQL Filter (string) »When you return a string from the filter method, it will be passed as the rawFilter parameter to all QueryService methods (query, findById, createOne, updateOne, deleteOne, etc.). This allows you to use raw SQL expressions for complex filtering scenarios.
Example: Using sql.raw with Drizzle
Section intitulée « Example: Using sql.raw with Drizzle »If you’re using Drizzle ORM, you can use sql.raw() to create raw SQL expressions:
import { ObjectType, Field } from '@lib/type-graphql';import { ContextFilter, OperationContext } from '@lib/type-graphql-query';import { sql } from 'drizzle-orm';
@ObjectType()@ContextFilter<Product>((ctx: OperationContext) => { const userId = ctx.resolverData.context.userId;
// Return a raw SQL filter string // This will be passed as rawFilter to QueryService methods return sql`user_id = ${userId} AND status = 'active'`.toQuery().sql;})export class Product { @Field() id!: string;
@Field() name!: string;
@Field() userId!: string;
@Field() status!: string;}Example: Complex Raw SQL Filter
Section intitulée « Example: Complex Raw SQL Filter »For more complex scenarios, you can build raw SQL filters dynamically:
import { ObjectType, Field } from '@lib/type-graphql';import { ContextFilter, OperationContext } from '@lib/type-graphql-query';import { sql } from 'drizzle-orm';
@ObjectType()@ContextFilter<Product>((ctx: OperationContext) => { const userId = ctx.resolverData.context.userId; const workspaceId = ctx.resolverData.context.workspaceId;
// Build a complex raw SQL filter if (ctx.operationType === 'READ') { // Users can read products they own or products shared with them return sql` (user_id = ${userId} OR workspace_id = ${workspaceId}) AND deleted_at IS NULL `.toQuery().sql; }
// For mutations, use a simpler filter return sql`user_id = ${userId}`.toQuery().sql;})export class Product { @Field() id!: string;
@Field() name!: string;
@Field() userId!: string;
@Field() workspaceId!: string;}Example: Returning Different Types
Section intitulée « Example: Returning Different Types »@ContextFilter<Product>((ctx: OperationContext) => { // Return a filter object (most common) if (ctx.operationType === 'READ') { return { userId: { eq: ctx.resolverData.context.userId } }; }
// Return a raw SQL string for complex filtering if (ctx.operationType === 'UPDATE') { return sql`user_id = ${ctx.resolverData.context.userId} AND status != 'archived'`.toQuery().sql; }
// Return undefined to apply no filter if (ctx.resolverData.context.user?.isAdmin) { return undefined; }
// Return null (same as undefined) return null;})Examples
Section intitulée « Examples »Multi-tenant Filtering
Section intitulée « Multi-tenant Filtering »Automatically filter by workspace for multi-tenant applications:
import { ObjectType, Field } from '@lib/type-graphql';import { ContextFilter, OperationContext } from '@lib/type-graphql-query';
@ObjectType()@ContextFilter<Order>((ctx: OperationContext) => { const workspaceId = ctx.resolverData.context.workspaceId;
// Automatically filter by workspace for all operations if (workspaceId) { return { workspaceId: { eq: workspaceId }, }; }
// No workspace context, return no results return { id: { eq: null } };})export class Order { @Field() id!: string;
@Field() workspaceId!: string;}Soft Delete Filtering
Section intitulée « Soft Delete Filtering »Automatically exclude soft-deleted items:
import { ObjectType, Field } from '@lib/type-graphql';import { ContextFilter, OperationContext } from '@lib/type-graphql-query';
@ObjectType()@ContextFilter<Product>((ctx: OperationContext) => { // Automatically exclude soft-deleted items for read operations // The withDeleted flag in the query will override this if (ctx.operationType === 'READ') { return { deletedAt: { is: null }, }; }
// For mutations, allow access to soft-deleted items return undefined;})export class Product { @Field() id!: string;
@Field() name!: string;
@Field({ nullable: true }) deletedAt?: Date;}Operation-Specific Filtering
Section intitulée « Operation-Specific Filtering »Apply different filters based on the operation:
import { ObjectType, Field } from '@lib/type-graphql';import { ContextFilter, OperationContext } from '@lib/type-graphql-query';
@ObjectType()@ContextFilter<Product>((ctx: OperationContext) => { const userId = ctx.resolverData.context.userId;
switch (ctx.operationType) { case 'READ': // Users can read their own products and public products return { or: [{ userId: { eq: userId } }, { isPublic: { eq: true } }], };
case 'CREATE': // Users can only create products for themselves return { userId: { eq: userId }, };
case 'UPDATE': case 'DELETE': // Users can only modify their own products return { userId: { eq: userId }, };
case 'UPSERT': // Users can upsert their own products return { userId: { eq: userId }, };
default: return undefined; }})export class Product { @Field() id!: string;
@Field() name!: string;
@Field() userId!: string;
@Field() isPublic!: boolean;}Many vs One Operations
Section intitulée « Many vs One Operations »Differentiate between single and multiple item operations:
import { ObjectType, Field } from '@lib/type-graphql';import { ContextFilter, OperationContext } from '@lib/type-graphql-query';
@ObjectType()@ContextFilter<Product>((ctx: OperationContext) => { const userId = ctx.resolverData.context.userId;
// For single item operations, allow access to own items if (!ctx.many) { return { userId: { eq: userId }, }; }
// For multiple item operations, apply additional restrictions return { and: [{ userId: { eq: userId } }, { status: { neq: 'archived' } }], };})export class Product { @Field() id!: string;
@Field() name!: string;
@Field() userId!: string;
@Field() status!: string;}Combining with User Filters
Section intitulée « Combining with User Filters »The @ContextFilter() automatically combines with user-provided filters using AND logic:
# User queryquery { products(filter: { name: { like: "Egg%" } }) { id name }}The actual filter applied will be:
{ and: [ { userId: { eq: currentUserId } }, // From @ContextFilter { name: { like: 'Egg%' } }, // From user query ];}Type Safety
Section intitulée « Type Safety »The TContextType must extend ExecutionContext to ensure type safety. The context is accessible directly via resolverData.context without needing a separate executionContext parameter.
When using module augmentation to extend ExecutionContext, TypeScript will automatically infer the extended type:
// In your execution-context.d.ts filedeclare module '@lib/type-graphql-query' { interface ExecutionContext { workspaceId?: string | null; platformRole?: string; }}
// In your DTO file@ContextFilter<Product>((ctx: OperationContext) => { // TypeScript automatically knows about extended ExecutionContext properties const userId = ctx.resolverData.context.userId; // ✅ Type-safe const workspaceId = ctx.resolverData.context.workspaceId; // ✅ Type-safe const platformRole = ctx.resolverData.context.platformRole; // ✅ Type-safe
return { userId: { eq: userId }, workspaceId: { eq: workspaceId }, };})export class Product { // ...}Best Practices
Section intitulée « Best Practices »- Always specify the DTO type in the decorator - Use
@ContextFilter<YourDTO>((ctx: OperationContext) => ...)for better type inference - Annotate the context parameter - Use
(ctx: OperationContext)to ensure TypeScript recognizes extended ExecutionContext properties - Return
undefinedornullfor no filter - This is clearer than returning an empty object - Use operation-specific logic - Different operations may need different filters
- Consider performance - Complex filters can impact query performance
- Test with different contexts - Ensure filters work correctly for different user roles and scenarios
- Prefer Filter objects over raw SQL - Use
Filter<DTO>objects when possible, as they are type-safe and database-agnostic - Use raw SQL filters carefully - When using
stringreturn type forrawFilter:- Always use parameterized queries (e.g.,
sql.raw()with Drizzle) - Never concatenate user input directly into SQL strings
- Test thoroughly to ensure the raw SQL works with your QueryService implementation
- Consider the database-specific SQL syntax if you support multiple databases
- Always use parameterized queries (e.g.,
Quick Reference
Section intitulée « Quick Reference »| Return Type | Description | Use Case |
|---|---|---|
Filter<DTO> |
Filter object | Standard filtering (recommended) |
string |
Raw SQL filter | Complex filtering scenarios |
undefined |
No filter | Allow all records |
null |
No filter | Allow all records |
Next Steps
Section intitulée « Next Steps »Continue learning:
- 🔍 Queries - Query operations
- ✏️ Mutations - Mutation operations
- 🔐 Authorization - Authorization and permissions
- 🔗 Relations - Entity relationships
Related Topics
Section intitulée « Related Topics »- Queries - Query operations
- Mutations - Mutation operations
- Authorization - Authorization and permissions
- Type GraphQL Query Overview - Complete overview