Aller au contenu

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.

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.

  • params.context - The execution context (user, locale, etc.)
  • params.query - The query to filter, page, and sort results
  • params.rawFilter - Optional raw SQL filter string (advanced usage)
  • params.opts - Optional query options (resolveInfo for 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' }],
},
});

Find a record by its id.

  • params.context - The execution context
  • params.id - The id of the record to find
  • params.rawFilter - Optional raw SQL filter string
  • params.locale - Optional locale for translations
  • params.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.

  • params.context - The execution context
  • params.id - The id of the record to find
  • params.rawFilter - Optional raw SQL filter string
  • params.locale - Optional locale for translations
  • params.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
}

Create a single record.

  • params.context - The execution context
  • params.item - A partial DTO to persist
  • params.rawFilter - Optional raw SQL filter string (for validation)
  • params.locale - Optional locale for translations
  • params.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',
});

Create multiple records.

  • params.context - The execution context
  • params.items - An array of partial DTOs to persist
  • params.rawFilter - Optional raw SQL filter string (for validation)
  • params.locale - Optional locale for translations
  • params.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' },
],
});

Update a single record.

  • params.context - The execution context
  • params.id - The id of the record to update
  • params.update - The update to apply
  • params.rawFilter - Optional raw SQL filter string (for validation)
  • params.locale - Optional locale for translations
  • params.opts - Additional options (filter, resolveInfo, transaction, etc.)

The updated DTO

const updated = await queryService.updateOne({
context,
id: '123',
update: {
status: 'ARCHIVED',
price: 149.99,
},
});

Update multiple records based on a filter.

  • params.context - The execution context
  • params.update - The update to apply
  • params.filter - A filter used to find the records to update
  • params.rawFilter - Optional raw SQL filter string (for validation)
  • params.locale - Optional locale for translations
  • params.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.updatedCount

Upsert a single record (create if not exists, update if exists).

  • params.context - The execution context
  • params.item - The item to upsert
  • params.rawFilter - Optional raw SQL filter string (for validation)
  • params.locale - Optional locale for translations
  • params.opts - Additional options (transaction, etc.)

The upserted DTO

const product = await queryService.upsertOne({
context,
item: {
id: '123',
name: 'Product Name',
status: 'ACTIVE',
},
});

Upsert multiple records.

  • params.context - The execution context
  • params.items - An array of items to upsert
  • params.rawFilter - Optional raw SQL filter string (for validation)
  • params.locale - Optional locale for translations
  • params.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' },
],
});

Delete a single record by id.

  • params.context - The execution context
  • params.id - The id of the record to delete
  • params.rawFilter - Optional raw SQL filter string (for validation)
  • params.locale - Optional locale
  • params.withDeleted - Whether to include soft-deleted records
  • params.opts - Additional options (filter, transaction, etc.)

The deleted DTO

const deleted = await queryService.deleteOne({
context,
id: '123',
});

Delete multiple records using a filter.

  • params.context - The execution context
  • params.filter - The filter to find records to delete
  • params.rawFilter - Optional raw SQL filter string (for validation)
  • params.locale - Optional locale
  • params.withDeleted - Whether to include soft-deleted records
  • params.opts - Additional options (transaction, etc.)

An object with a deletedCount field

const result = await queryService.deleteMany({
context,
filter: { status: { eq: 'ARCHIVED' } },
});
// result.deletedCount

Perform an aggregate query. Supported aggregate functions are groupBy, count, sum, avg, min, and max.

  • params.context - The execution context
  • params.filter - Additional filter to apply
  • params.aggregate - The aggregate query
  • params.rawFilter - Optional raw SQL filter string
  • params.search - Optional search string
  • params.withDeleted - Whether to include soft-deleted records
  • params.opts - Additional options (transaction, etc.)

An array of aggregate responses

{
count: ['id'],
sum: ['price'],
avg: ['price'],
min: ['id', 'name'],
max: ['id', 'name'],
groupBy: ['categoryId']
}
[
{
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' },
},
];
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.

  • params.context - The execution context
  • params.filter - The filter to count records by
  • params.rawFilter - Optional raw SQL filter string
  • params.search - Optional search string
  • params.withDeleted - Whether to include soft-deleted records
  • params.opts - Additional options (transaction, etc.)

A count of records that match the filter

const count = await queryService.count({
context,
filter: { status: { eq: 'ACTIVE' } },
});

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.

  • params.context - The execution context
  • params.items - The DTO or array of DTOs to fill
  • params.rawFilter - Optional raw SQL filter string
  • params.locale - Optional locale for translations
  • params.withDeleted - Whether to include soft-deleted records
  • params.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 },
});

Listen for changes to the DTO. This is used for subscriptions and real-time updates.

  • cb - The callback function to receive changes

void

queryService.listenChanges((changes) => {
console.log('Changes:', changes);
// Handle changes for subscriptions
});

All methods accept a context parameter that provides:

  • userId - The current user’s ID
  • locale - The current locale for translations
  • Additional properties extended via module augmentation

An optional raw SQL filter string that can be used for complex filtering scenarios. This is typically set by @ContextFilter decorators.

An optional locale string for fetching translated fields. If not provided, the locale from the context will be used.

Whether to include soft-deleted records in the query. Defaults to false.

Additional options that vary by method:

  • resolveInfo - GraphQL resolve info for field selection
  • transaction - Database transaction for atomic operations
  • filter - Additional filter constraints
  • Method-specific options

The ProxyQueryService delegates to another query service. Use it to override specific methods without extending the full implementation.

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;
}
}

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.

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;
}
}