QueryService
The QueryService interface is the core abstraction used to query and modify records in type-graphql-query. All resolvers use a QueryService implementation to interact with your data layer.
Overview
Section intitulée « Overview »The QueryService interface defines methods for:
- Querying records (with filtering, paging, and sorting)
- Creating, updating, upserting, and deleting records
- Aggregating data
- Completing partial results (
fillMissingFields) - Listening to changes
Relations are loaded eagerly through query, findById, and fillMissingFields when
opts.resolveInfo is provided. There are no separate relation query methods on the interface.
All methods accept an ExecutionContext parameter that provides access to user information, locale, and other context data.
Query for multiple records, with filtering, paging, and sorting.
Arguments
Section intitulée « Arguments »params.context- The execution context (user, locale, etc.)params.query- The query to filter, page, and sort resultsparams.rawFilter- Optional raw SQL filter string (advanced usage)params.opts- Optional query options (resolveInfofor field/relation selection, transaction, etc.)
A promise that resolves to an array of DTOs
const products = await queryService.query({ context, query: { filter: { status: { eq: 'ACTIVE' } }, paging: { limit: 10, offset: 0 }, sorting: [{ field: 'name', direction: 'ASC' }], },});findById
Section intitulée « findById »Find a record by its id.
Arguments
Section intitulée « Arguments »params.context- The execution contextparams.id- The id of the record to findparams.rawFilter- Optional raw SQL filter stringparams.locale- Optional locale for translationsparams.opts- Additional options (filter, resolveInfo, transaction, etc.)params.withDeleted- Whether to include soft-deleted records
The DTO or undefined if not found
const product = await queryService.findById({ context, id: '123', locale: 'fr',});Get a record by its id or throw a NotFoundException if not found.
Arguments
Section intitulée « Arguments »params.context- The execution contextparams.id- The id of the record to findparams.rawFilter- Optional raw SQL filter stringparams.locale- Optional locale for translationsparams.opts- Additional options (filter, resolveInfo, transaction, etc.)params.withDeleted- Whether to include soft-deleted records
The DTO or a rejected promise with a NotFoundException
try { const product = await queryService.getById({ context, id: '123', });} catch (error) { // Handle NotFoundException}createOne
Section intitulée « createOne »Create a single record.
Arguments
Section intitulée « Arguments »params.context- The execution contextparams.item- A partial DTO to persistparams.rawFilter- Optional raw SQL filter string (for validation)params.locale- Optional locale for translationsparams.opts- Additional options (transaction, etc.)
The saved DTO
const product = await queryService.createOne({ context, item: { name: 'New Product', status: 'ACTIVE', price: 99.99, }, locale: 'fr',});createMany
Section intitulée « createMany »Create multiple records.
Arguments
Section intitulée « Arguments »params.context- The execution contextparams.items- An array of partial DTOs to persistparams.rawFilter- Optional raw SQL filter string (for validation)params.locale- Optional locale for translationsparams.opts- Additional options (transaction, etc.)
The saved DTOs
const products = await queryService.createMany({ context, items: [ { name: 'Product 1', status: 'ACTIVE' }, { name: 'Product 2', status: 'ACTIVE' }, ],});updateOne
Section intitulée « updateOne »Update a single record.
Arguments
Section intitulée « Arguments »params.context- The execution contextparams.id- The id of the record to updateparams.update- The update to applyparams.rawFilter- Optional raw SQL filter string (for validation)params.locale- Optional locale for translationsparams.opts- Additional options (filter, resolveInfo, transaction, etc.)
The updated DTO
const updated = await queryService.updateOne({ context, id: '123', update: { status: 'ARCHIVED', price: 149.99, },});updateMany
Section intitulée « updateMany »Update multiple records based on a filter.
Arguments
Section intitulée « Arguments »params.context- The execution contextparams.update- The update to applyparams.filter- A filter used to find the records to updateparams.rawFilter- Optional raw SQL filter string (for validation)params.locale- Optional locale for translationsparams.opts- Additional options (transaction, etc.)
An object with the updatedCount
const result = await queryService.updateMany({ context, update: { status: 'ARCHIVED' }, filter: { categoryId: { eq: 'cat-123' } },});// result.updatedCountupsertOne
Section intitulée « upsertOne »Upsert a single record (create if not exists, update if exists).
Arguments
Section intitulée « Arguments »params.context- The execution contextparams.item- The item to upsertparams.rawFilter- Optional raw SQL filter string (for validation)params.locale- Optional locale for translationsparams.opts- Additional options (transaction, etc.)
The upserted DTO
const product = await queryService.upsertOne({ context, item: { id: '123', name: 'Product Name', status: 'ACTIVE', },});upsertMany
Section intitulée « upsertMany »Upsert multiple records.
Arguments
Section intitulée « Arguments »params.context- The execution contextparams.items- An array of items to upsertparams.rawFilter- Optional raw SQL filter string (for validation)params.locale- Optional locale for translationsparams.opts- Additional options (transaction, etc.)
The upserted DTOs
const products = await queryService.upsertMany({ context, items: [ { id: '1', name: 'Product 1' }, { id: '2', name: 'Product 2' }, ],});deleteOne
Section intitulée « deleteOne »Delete a single record by id.
Arguments
Section intitulée « Arguments »params.context- The execution contextparams.id- The id of the record to deleteparams.rawFilter- Optional raw SQL filter string (for validation)params.locale- Optional localeparams.withDeleted- Whether to include soft-deleted recordsparams.opts- Additional options (filter, transaction, etc.)
The deleted DTO
const deleted = await queryService.deleteOne({ context, id: '123',});deleteMany
Section intitulée « deleteMany »Delete multiple records using a filter.
Arguments
Section intitulée « Arguments »params.context- The execution contextparams.filter- The filter to find records to deleteparams.rawFilter- Optional raw SQL filter string (for validation)params.locale- Optional localeparams.withDeleted- Whether to include soft-deleted recordsparams.opts- Additional options (transaction, etc.)
An object with a deletedCount field
const result = await queryService.deleteMany({ context, filter: { status: { eq: 'ARCHIVED' } },});// result.deletedCountaggregate
Section intitulée « aggregate »Perform an aggregate query. Supported aggregate functions are groupBy, count, sum, avg, min, and max.
Arguments
Section intitulée « Arguments »params.context- The execution contextparams.filter- Additional filter to applyparams.aggregate- The aggregate queryparams.rawFilter- Optional raw SQL filter stringparams.search- Optional search stringparams.withDeleted- Whether to include soft-deleted recordsparams.opts- Additional options (transaction, etc.)
An array of aggregate responses
Example AggregateQuery
Section intitulée « Example AggregateQuery »{ count: ['id'], sum: ['price'], avg: ['price'], min: ['id', 'name'], max: ['id', 'name'], groupBy: ['categoryId']}Example AggregateResponse
Section intitulée « Example AggregateResponse »[ { count: { id: 5 }, sum: { price: 1000 }, avg: { price: 200 }, min: { id: 1, name: 'A Product' }, max: { id: 5, name: 'Z Product' }, groupBy: { categoryId: 'cat-123' }, },];Example Usage
Section intitulée « Example Usage »const aggregates = await queryService.aggregate({ context, filter: { status: { eq: 'ACTIVE' } }, aggregate: { count: ['id'], sum: ['price'], avg: ['price'], },});Count the number of records that match the filter.
Arguments
Section intitulée « Arguments »params.context- The execution contextparams.filter- The filter to count records byparams.rawFilter- Optional raw SQL filter stringparams.search- Optional search stringparams.withDeleted- Whether to include soft-deleted recordsparams.opts- Additional options (transaction, etc.)
A count of records that match the filter
const count = await queryService.count({ context, filter: { status: { eq: 'ACTIVE' } },});fillMissingFields
Section intitulée « fillMissingFields »Fill missing fields in DTO(s) based on the current query context. This is useful for ensuring all requested fields are populated, especially for relations and translations.
Arguments
Section intitulée « Arguments »params.context- The execution contextparams.items- The DTO or array of DTOs to fillparams.rawFilter- Optional raw SQL filter stringparams.locale- Optional locale for translationsparams.withDeleted- Whether to include soft-deleted recordsparams.opts- Additional options (resolveInfo, transaction, etc.)
The DTO or array of DTOs with missing fields filled
const filledProducts = await queryService.fillMissingFields({ context, items: products, locale: 'fr', opts: { resolveInfo },});listenChanges
Section intitulée « listenChanges »Listen for changes to the DTO. This is used for subscriptions and real-time updates.
Arguments
Section intitulée « Arguments »cb- The callback function to receive changes
void
queryService.listenChanges((changes) => { console.log('Changes:', changes); // Handle changes for subscriptions});Common Parameters
Section intitulée « Common Parameters »context: ExecutionContext
Section intitulée « context: ExecutionContext »All methods accept a context parameter that provides:
userId- The current user’s IDlocale- The current locale for translations- Additional properties extended via module augmentation
rawFilter?: string
Section intitulée « rawFilter?: string »An optional raw SQL filter string that can be used for complex filtering scenarios. This is typically set by @ContextFilter decorators.
locale?: string
Section intitulée « locale?: string »An optional locale string for fetching translated fields. If not provided, the locale from the context will be used.
withDeleted?: boolean
Section intitulée « withDeleted?: boolean »Whether to include soft-deleted records in the query. Defaults to false.
opts?: Options
Section intitulée « opts?: Options »Additional options that vary by method:
resolveInfo- GraphQL resolve info for field selectiontransaction- Database transaction for atomic operationsfilter- Additional filter constraints- Method-specific options
Service Helpers
Section intitulée « Service Helpers »ProxyQueryService
Section intitulée « ProxyQueryService »The ProxyQueryService delegates to another query service. Use it to override specific methods without extending the full implementation.
Example: Mutation Logger
Section intitulée « Example: Mutation Logger »You can use the ProxyQueryService to create a generic query service that will time and log a message every time a create, update, or delete method is called.
import { ProxyQueryService, QueryService } from '@lib/type-graphql-query';import { Logger } from '@nestjs/common';
export class MutationLoggerQueryService< DTO, C = DeepPartial<DTO>, U = DeepPartial<DTO>,> extends ProxyQueryService<DTO, C, U> { private readonly logger: Logger;
constructor(label: string, queryService: QueryService<DTO, C, U>) { super(queryService); this.logger = new Logger(label); }
async createOne(params: { context: ExecutionContext; item: C; locale?: string; opts?: UpsertOptions<DTO>; }): Promise<DTO> { return this.timedLog(`create one`, () => super.createOne(params)); }
async createMany(params: { context: ExecutionContext; items: C[]; locale?: string; opts?: UpsertOptions<DTO>; }): Promise<DTO[]> { return this.timedLog(`create many [itemCount=${params.items.length}]`, () => super.createMany(params), ); }
async updateOne(params: { context: ExecutionContext; id: string | number; update: U; locale?: string; opts?: UpdateOneOptions<DTO>; }): Promise<DTO> { return this.timedLog(`update one [id=${params.id}]`, () => super.updateOne(params)); }
async deleteOne(params: { context: ExecutionContext; id: number | string; locale?: string; opts?: DeleteOneOptions<DTO>; }): Promise<DTO> { return this.timedLog(`delete one [id=${params.id}]`, () => super.deleteOne(params)); }
private async timedLog<T>(message: string, fn: () => Promise<T>): Promise<T> { const start = Date.now(); const result = await fn(); const duration = Date.now() - start; this.logger.log(`${message} [duration=${duration}ms]`); return result; }}NoOpQueryService
Section intitulée « NoOpQueryService »The no-op query service is one that will throw a NotImplementedException for every method.
This is commonly used during testing when you want to mock out a service.
You can also use the NoOpQueryService as a base for a new query service that only supports a subset of operations.
Example: In-Memory QueryService
Section intitulée « Example: In-Memory QueryService »import { applyFilter, applyQuery, DeleteManyResponse, DeleteOneOptions, Filter, FindByIdOptions, GetByIdOptions, NoOpQueryService, Query, QueryService, UpdateManyResponse, UpdateOneOptions,} from '@lib/type-graphql-query';import { NotFoundException } from '@nestjs/common';
@QueryService(Product)export class InMemoryProductService extends NoOpQueryService<Product> { private records: Product[] = [];
async createMany(params: { context: ExecutionContext; items: Product[] }): Promise<Product[]> { this.records.push(...params.items); return Promise.resolve(params.items); }
async createOne(params: { context: ExecutionContext; item: Product }): Promise<Product> { this.records.push(params.item); return Promise.resolve(params.item); }
async updateMany(params: { context: ExecutionContext; update: Partial<Product>; filter: Filter<Product>; }): Promise<UpdateManyResponse> { const recordsToUpdate = await this.query({ context: params.context, query: { filter: params.filter }, }); recordsToUpdate.forEach((r) => Object.assign(r, params.update)); return { updatedCount: recordsToUpdate.length }; }
async updateOne(params: { context: ExecutionContext; id: string | number; update: Partial<Product>; opts?: UpdateOneOptions<Product>; }): Promise<Product> { const record = await this.getById({ context: params.context, id: params.id, opts: params.opts, }); return Object.assign(record, params.update); }
async deleteMany(params: { context: ExecutionContext; filter: Filter<Product>; }): Promise<DeleteManyResponse> { const recordIds = ( await this.query({ context: params.context, query: { filter: params.filter } }) ).map((r) => r.id); this.records = this.records.filter((r) => !recordIds.includes(r.id)); return Promise.resolve({ deletedCount: recordIds.length }); }
async deleteOne(params: { context: ExecutionContext; id: number | string; opts?: DeleteOneOptions<Product>; }): Promise<Product> { const record = await this.getById({ context: params.context, id: params.id, opts: params.opts, }); this.records = this.records.filter((r) => r.id !== record.id); return Promise.resolve(record); }
async findById(params: { context: ExecutionContext; id: string | number; opts?: FindByIdOptions<Product>; }): Promise<Product | undefined> { const record = applyFilter(this.records, params.opts?.filter ?? {}).find( (r) => r.id === params.id, ); return Promise.resolve(record); }
async getById(params: { context: ExecutionContext; id: string | number; opts?: GetByIdOptions<Product>; }): Promise<Product> { const record = await this.findById(params); if (!record) { throw new NotFoundException(`Unable to find Product with id ${params.id}`); } return record; }
async query(params: { context: ExecutionContext; query: Query<Product> }): Promise<Product[]> { return Promise.resolve(applyQuery(this.records, params.query)); }
async count(params: { context: ExecutionContext; filter: Filter<Product> }): Promise<number> { const found = await this.query({ context: params.context, query: { filter: params.filter } }); return found.length; }}