Skip to main content

QueryService

The core package defines a QueryService which is used to query and modify records.

Methods

The following methods are defined on the QueryService

query

Query for multiple records, with a filter, paging and sorting.

Arguments

  • query: Query<DTO> - The query to filter, page, and sort results.

Returns

An array of DTOs

findById

Find a record by its id.

Arguments

  • id: string | number - The id of the record to find

Returns

The DTO or undefined

getById

  • get a record by its id or return a rejected promise with a NotFound error.

Arguments

  • id: string | number - The id of the record to find

Returns

The DTO or a NotFoundException.

createMany

Create multiple records.

Arguments

  • items: DeepPartial<DTO>[] - An array of partial DTOs to persist

Returns

The saved DTOs.

createOne

Create a single DTO.

Arguments

  • item: DeepPartial<DTO> - A partial of the DTO to persist

Returns

The saved DTO

updateMany

Update multiple records based on a filter.

Arguments

  • update: DeepPartial<DTO> - The update to apply
  • filter: Filter<DTO> - A Filter used to find the records to update

Returns

An object with the updatedCount

note

UpdatedCount may be 0 if the database does not return the number of rows updated.

updateOne

Update a single record.

Arguments

  • id: string | number - The id of the record to update
  • update: DeepPartial<DTO> - The update to apply

Returns

The updated DTO

deleteMany

Delete multiple records.

Arguments

  • filter: Filter<DTO> - The filter to find the records to delete.

Returns

An object with a deletedCount field.

note

deletedCount may be 0 if the database does not return the number of rows deleted.

:::

deleteOne

Delete a single record.

Arguments

  • id: number | string

Returns

Promise<DTO>

aggregate

Performs an aggregate query, supported aggregate functions are groupBy, count, sum, avg, min, and max

Arguments

  • filter: Filter<DTO> - Additional filter to apply
  • aggregate: AggregateQuery<DTO> - The aggregate query

Example AggregateQuery

{
count: ['id'],
sum: ['priority'],
avg: ['priority'],
min: ['id', 'title'],
max: ['id', 'title']
}

Returns

An array of aggregate responses.

Example AggregateResponse

[
{
count: { id: 5 },
sum: { id: 10 },
avg: { id: 2.5 },
min: {id: 1, title: 'A Title'},
max: {id: 4, title: 'Z Title'}
}
]

count

Count the number of records that match the filter

Arguments

  • filter: Filter<DTO> - The filter a count records by

Returns

A count of records that match the filter

queryRelations

Query for relations

Arguments

  • RelationClass: Class<Relation> - The Class type of the relation
  • relationName: string - The name of the relation
  • dto: DTO | DTO[] - The dto(s) to find the relations for.
  • query: Query<Relation> - Additional query to use when querying for relations.

Returns

If querying for relations for a single DTO an array of relations will be returned. If querying for relations for multiple DTOs a map where the key is the DTO and the value is the relations for the DTO.

aggregateRelations

Performs an aggregate query for the relations of a DTO.

Arguments

  • RelationClass: Class<Relation> - The Class type of the relation
  • relationName: string - The name of the relation
  • dto: DTO | DTO[] - The dto(s) to aggregate the relations for.
  • filter: Filter<Relation> - A filter to apply when aggregating relations
  • aggregate: AggregateQuery<Relation> - The aggregateQuery for the relations

Returns

If aggregating relations for a single DTO an AggregateResponse for the dtos relations will be returned If aggregating relations for multiple DTOs a map where the key is the DTO and the value is the AggregateResponse for the dtos relations.

countRelations

Counts the number of relations.

Arguments

  • RelationClass: Class<Relation> - The Class type of the relation
  • relationName: string - The name of the relation
  • dto: DTO | DTO[] - The dto(s) to count the relations for.
  • filter: Filter<Relation>- A filter to apply when counting relations

Returns

If counting relations for a single DTO the relation count will be returned If counting relations for multiple DTOs a map where the key is the DTO and the value is relation count for the dtos relations.

findRelation

Find a single relation for the DTO(s).

Arguments

  • RelationClass: Class<Relation> - The Class type of the relation
  • relationName: string - The name of the relation
  • dto: DTO | DTO[] - The dto(s) to find the relation for.
  • opts?: FindRelationOptions<Relation> - Additional options to find a relation by.

Returns

If finding a relation for a single DTO the relation or undefineding returned If finding a relation for multiple DTOs a map where the key is the DTO and the value is the relation or undefined.

addRelations

Adds relations to a DTO

Arguments

  • relationName: string - The name of the relation
  • id: string | number - The id of the DTO to add the relations to
  • relationIds: (string | number)[] - The ids of the relations to add
  • opts?: ModifyRelationOptions<DTO, Relation> - Additional options apply when adding relations

Returns

The DTO the relations were added to.

setRelations

Sets relations on a DTO

Arguments

  • relationName: string - The name of the relation
  • id: string | number - The id of the DTO to add the relations to
  • relationIds: (string | number)[] - The ids of the relations to set. If the relationIds is empty the all relations will be removed.
  • opts?: ModifyRelationOptions<DTO, Relation> - Additional options apply when adding relations

Returns

The DTO the relations were added to.

setRelation

Set a relation on a DTO

Arguments

  • relationName: string - The name of the relation
  • id: string | number - The id of the DTO to add the relations to
  • relationId: string | number - The id of the relation to set on the DTO
  • opts?: ModifyRelationOptions<DTO, Relation> - Additional options apply when setting the relation

Returns

The DTO the relation was set on.

removeRelations

Removes multiple relations from a DTO

Arguments

  • relationName: string - The name of the relation
  • id: string | number - The id of the DTO to remove the relations from.
  • relationIds: (string | number)[] - The ids of the relations to remove
  • opts?: ModifyRelationOptions<DTO, Relation> - Additional options to apply when removing relations

Returns

The DTO the relations were removed from

removeRelation

Remove a relation from a DTO

Arguments

  • relationName: string - The name of the relation
  • id: string | number - The id of the DTO to remove the relation from.
  • relationId: string | number - The id of the relation to remove
  • opts?: ModifyRelationOptions<DTO, Relation> - Additional options to apply when removing the relation.

Returns

The DTO the relation was removed from.

Service Helpers

You can create your own service to use with the CRUDResolver as long as it implements the QueryService interface.

There are a number of persistence QueryServices that are provided out of the box.

In addition to the persistence QueryServices @ptc-org/nestjs-query-core provides a few helper services that can be used for more complex use cases.

When designing the base services we have chosen composition over inheritance. This approach lends itself well to modeling complex services without repeating yourself.

RelationQueryService

The RelationQueryService was originally designed for federation, but has proven itself useful in representing virtual relations. A virtual relation(s) is anything that can be queried through a query service.

To create additional relations through a RelationQueryService you need to provide the following

  • A QueryService that can be used to fetch the relation
  • A query function that accepts the parent DTO to fetch the relation for and returns a Query to fetch the relations.
info

Relations defined using the RelationQueryService are readonly!

When a relation query method is called it will:

  • First check if the relation is a virtual relation, if true it will invoke the query option to generate a query that will be passed to the queryService to fetch the relations.
  • If the relation is not a virtual relation it will proxy to the original query service to query for the relation.

For example, we could wrap the TodoItem query service and add a completedSubTasks relation.

todo-item/todo-item.service.ts
import { InjectQueryService, QueryService, RelationQueryService } from '@ptc-org/nestjs-query-core';
import { TodoItemEntity } from './todo-item.entity';
import { SubTaskEntity } from '../sub-task/sub-task.entity';

export class TodoItemService extends RelationQueryService<TodoItemEntity> {
constructor(
@InjectQueryService(TodoItemEntity) queryService: QueryService<TodoItemEntity>,
@InjectQueryService(SubTaskEntity) subTaskQueryService: QueryService<SubTaskEntity>,
) {
// provide the original query service so all relations defined in the ORM work
super(queryService, {
// specify the virtual relations
completedSubTasks: {
// provide the service that will be used to query the relation
service: subTaskQueryService,
// the query method accepts a todoItem that can be used to filter the relations
query(todoItem) {
// filter for all relations that belong to the todoItem and are completed
return { filter: { todoItemId: { eq: todoItem.id }, completed: { is: true } } };
},
},
});
}
}

Once the relation is defined in the query service we can add it to our DTO to expose it in our schema.

todo-item/todo-item.dto.ts
import { FilterableField, IDField, FilterableConnection, KeySet } from '@ptc-org/nestjs-query-graphql';
import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql';
import { SubTaskDTO } from '../../sub-task/dto/sub-task.dto';

@ObjectType('TodoItem')
@KeySet(['id'])
@FilterableConnection('subTasks', () => SubTaskDTO)
@FilterableConnection('completedSubTasks', () => SubTaskDTO)
export class TodoItemDTO {
@IDField(() => ID)
id!: number;

@FilterableField()
title!: string;

@FilterableField({ nullable: true })
description?: string;

@FilterableField()
completed!: boolean;

@FilterableField(() => GraphQLISODateTime)
created!: Date;

@FilterableField(() => GraphQLISODateTime)
updated!: Date;
}

Next we need to export the SubTask query service from the SubTaskModule so we can resolve it in the TodoItemService.

sub-task/sub-task.module.ts
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { Module } from '@nestjs/common';
import { SubTaskDTO } from './dto/sub-task.dto';
import { SubTaskEntity } from './sub-task.entity';

// define the persistence module so it can be exported
const nestjsQueryTypeOrmModule = NestjsQueryTypeOrmModule.forFeature([SubTaskEntity]);

@Module({
imports: [
NestjsQueryGraphQLModule.forFeature({
// import it in the graphql module
imports: [nestjsQueryTypeOrmModule],
resolvers: [
{
DTOClass: SubTaskDTO,
EntityClass: SubTaskEntity,
},
],
}),
// import it into the subTaskModule so it can be exported
nestjsQueryTypeOrmModule,
],
// export the persistence module so it can be used by the TodoItemService
exports: [nestjsQueryTypeOrmModule],
})
export class SubTaskModule {}

Now we can import the SubTaskModule into the TodoItemModule so the SubTask query service can be injected into the TodoItemService.

todo-item/todo-item.module.ts
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { Module } from '@nestjs/common';
import { TodoItemDTO } from './dto/todo-item.dto';
import { TodoItemAssembler } from './todo-item.assembler';
import { TodoItemEntity } from './todo-item.entity';
import { TodoItemResolver } from './todo-item.resolver';
import { TodoItemService } from './todo-item.service';
import { SubTaskModule } from '../sub-task/sub-task.module';

@Module({
providers: [TodoItemResolver],
imports: [
NestjsQueryGraphQLModule.forFeature({
// import the persistence module for the TodoItemEntity and the SubTaskModule
imports: [NestjsQueryTypeOrmModule.forFeature([TodoItemEntity]), SubTaskModule],
services: [TodoItemService],
assemblers: [TodoItemAssembler],
resolvers: [
{
DTOClass: TodoItemDTO,
ServiceClass: TodoItemService,
},
],
}),
],
})
export class TodoItemModule {}

The completedSubTasks relation is now available in your graphql schema.

ProxyQueryService

The ProxyQueryService is a query service that delegates to another query service. The ProxyQueryService can be used when you want to override certain methods of a query service without extending it.

This class is used internally by the RelationQueryService to override the relation methods for a QueryService

Lets use the ProxyQueryService to create a generic query service that will time and log a message everytime a create, update, or delete method is called.

To start lets define a MutationLoggerQueryService.

utilities/mutation-logger-query.service.ts
import { Logger, LoggerService } from '@nestjs/common';
import { DeepPartial } from '../common';
import { Filter, DeleteManyResponse, DeleteOneOptions, UpdateManyResponse, UpdateOneOptions } from '../interfaces';
import { ProxyQueryService } from './proxy-query.service';
import { QueryService } from './query.service';

export class MutationLoggerQueryService<DTO, C = DeepPartial<DTO>, U = DeepPartial<DTO>> extends ProxyQueryService<
DTO,
C,
U
> {
private readonly logger: LoggerService;

constructor(label: string, queryService: QueryService<DTO, C, U>) {
// call super witht the QueryService we will delegate to
super(queryService);
// create our logger
this.logger = new Logger(label);
}

// Override all the create, update, and delete methods to add the timed logging functionality
createMany(items: C[]): Promise<DTO[]> {
return this.timedLog(`create many [itemCount=${items.length}]`, () => super.createMany(items));
}

createOne(item: C): Promise<DTO> {
return this.timedLog(`create one`, () => super.createOne(item));
}

deleteMany(filter: Filter<DTO>): Promise<DeleteManyResponse> {
return this.timedLog(`delete many`, () => super.deleteMany(filter));
}

deleteOne(id: number | string, opts?: DeleteOneOptions<DTO>): Promise<DTO> {
return this.timedLog(`delete one [id=${id}]`, () => super.deleteOne(id, opts));
}

updateMany(update: U, filter: Filter<DTO>): Promise<UpdateManyResponse> {
return this.timedLog('update many', () => super.updateMany(update, filter));
}

updateOne(id: string | number, update: U, opts?: UpdateOneOptions<DTO>): Promise<DTO> {
return this.timedLog(`update one [id=${id}]`, () => super.updateOne(id, update, opts));
}

private async timedLog<T>(message, fn: () => Promise<T>): Promise<T> {
const start = new Date();
const result = await fn();
const duration = start.getTime() - new Date().getTime();
this.logger.log(`${message} [duration=${duration}]`);
return result;
}
}

We can now add timed logging to any query service.

Lets add it to our TodoItemQueryService

todo-item/todo-item.service.ts
import { InjectQueryService, QueryService } from '@ptc-org/nestjs-query-core';
import { MutationLoggerQueryService } from '../utilities/mutation-logger-query.service';
import { TodoItemEntity } from './todo-item.entity';

@QueryService(TodoItemEntity)
export class TodoItemService extends MutationLoggerQueryService<TodoItemEntity> {
constructor(@InjectQueryService(TodoItemEntity) service: QueryService<TodoItemEntity>) {
// provide the logger name and the service
super(TodoItemService.name, service);
}
}

Don't forget to use your custom query service in your module

todo-item/todo-item.module.ts
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { Module } from '@nestjs/common';
import { TodoItemDTO } from './dto/todo-item.dto';
import { TodoItemEntity } from './todo-item.entity';
import { TodoItemService } from './todo-item.service';

@Module({
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [NestjsQueryTypeOrmModule.forFeature([TodoItemEntity])],
services: [TodoItemService],
resolvers: [
{
DTOClass: TodoItemDTO,
ServiceClass: TodoItemService,
},
],
}),
],
})
export class TodoItemModule {}

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 a new query service that only supports a subset of operations.

In this example we'll create a simple query service that stores elements in an array but does not support relations or aggregations.

import {
applyFilter,
applyQuery,
DeleteManyResponse,
DeleteOneOptions,
Filter,
FindByIdOptions,
GetByIdOptions,
NoOpQueryService,
Query,
QueryService,
UpdateManyResponse,
UpdateOneOptions,
} from '@ptc-org/nestjs-query-core';
import { NotFoundException } from '@nestjs/common';
import { TodoItemEntity } from './todo-item.entity';

@QueryService(TodoItemEntity)
export class TodoItemService extends NoOpQueryService<TodoItemEntity> {
private records: TodoItemEntity[];

constructor() {
super();
this.records = [];
}

createMany(items: TodoItemEntity[]): Promise<TodoItemEntity[]> {
this.records.push(...items);
return Promise.resolve(items);
}

createOne(item: TodoItemEntity): Promise<TodoItemEntity> {
this.records.push(item);
return Promise.resolve(item);
}

async updateMany(update: Partial<TodoItemEntity>, filter: Filter<TodoItemEntity>): Promise<UpdateManyResponse> {
const recordsToUpdate = await this.query({ filter });
recordsToUpdate.forEach((r) => Object.assign(r, update));
return { updatedCount: recordsToUpdate.length };
}

async updateOne(
id: string | number,
update: Partial<TodoItemEntity>,
opts?: UpdateOneOptions<TodoItemEntity>,
): Promise<TodoItemEntity> {
const record = await this.getById(id, opts);
return Object.assign(record, update);
}

async deleteMany(filter: Filter<TodoItemEntity>): Promise<DeleteManyResponse> {
const recordIds = (await this.query({ filter })).map((r) => r.id);
this.records = this.records.filter((r) => !recordIds.includes(r.id));
return Promise.resolve({ deletedCount: recordIds.length });
}

async deleteOne(id: number | string, opts?: DeleteOneOptions<TodoItemEntity>): Promise<TodoItemEntity> {
const record = await this.getById(id, opts);
this.records = this.records.filter((r) => r.id !== record.id);
return Promise.resolve(record);
}

findById(id: string | number, opts?: FindByIdOptions<TodoItemEntity>): Promise<TodoItemEntity | undefined> {
const record = applyFilter(this.records, opts?.filter ?? {}).find((r) => r.id === id);
return Promise.resolve(record);
}

async getById(id: string | number, opts?: GetByIdOptions<TodoItemEntity>): Promise<TodoItemEntity> {
const record = await this.findById(id, opts);
if (!record) {
throw new NotFoundException(`Unable to find TodoItem with id ${id}`);
}
return record;
}

query(query: Query<TodoItemEntity>): Promise<TodoItemEntity[]> {
return Promise.resolve(applyQuery(this.records, query));
}

async count(filter: Filter<TodoItemEntity>): Promise<number> {
const found = await this.query({ filter });
return found.length;
}
}