Skip to content

NestJS: Query Service

Overview

RelayerService<TEntity, TEntities> is the data layer. It wraps a Relayer entity repository with typed CRUD methods and applies business-level defaults. Services work everywhere: controllers, cron jobs, event handlers, tests.

Module setup

See Getting Started for RelayerModule.forRoot(), forRootAsync(), and forFeature() setup.

Entity map

The entity map ties your entity classes together and enables relation-aware type inference across the entire application:

entities/entity-map.ts
import { CommentEntity } from './comment.entity';
import { PostEntity } from './post.entity';
import { UserEntity } from './user.entity';
export const entities = {
users: UserEntity,
posts: PostEntity,
comments: CommentEntity,
};
export type EM = typeof entities;

Pass EM as the second generic to RelayerService, RelayerController, RelayerHooks, @CrudController, etc. This gives you:

  • Autocomplete for relation fields (author.fullName, comments)
  • Typed where, select, orderBy that include computed/derived fields from related entities
  • Cross-entity access via this.r.users, this.r.comments

Creating a service

import { Injectable } from '@nestjs/common';
import { InjectRelayer, RelayerService } from '@relayerjs/nestjs-crud';
import type { RelayerInstance } from '@relayerjs/nestjs-crud';
import { PostEntity, type EM } from '../entities';
@Injectable()
export class PostsService extends RelayerService<PostEntity, EM> {
constructor(@InjectRelayer() r: RelayerInstance<EM>) {
super(r, PostEntity);
}
async findPublished() {
return this.findMany({
where: { published: true },
select: { id: true, title: true },
});
}
}

CRUD methods

All methods are fully typed based on TEntity and TEntities. Types like Where, Select, OrderBy include computed, derived, and relation fields.

findMany

findMany<TSelect extends Select<TEntity, TEntities> | undefined>(
options?: ManyOptions<TEntity, TEntities> & { select?: TSelect },
): Promise<SelectResult<Model<TEntity, TEntities>, TSelect>[]>
// Full entity
const posts = await this.findMany();
// With filtering and selection
const posts = await this.findMany({
where: { published: true, author: { fullName: { contains: 'John' } } },
select: { id: true, title: true, author: { fullName: true } },
orderBy: { field: 'createdAt', order: 'desc' },
limit: 10,
offset: 0,
});
// Return type narrowed to: { id: number; title: string; author: { fullName: string } }[]

findFirst

findFirst<TSelect extends Select<TEntity, TEntities> | undefined>(
options?: FirstOptions<TEntity, TEntities> & { select?: TSelect },
): Promise<SelectResult<Model<TEntity, TEntities>, TSelect> | null>
const post = await this.findFirst({
where: { id: 1 },
select: { id: true, title: true },
});
// { id: number; title: string } | null

count

count(options?: WhereOptions<TEntity, TEntities>): Promise<number>
const total = await this.count({ where: { published: true } });

create

create(options: { data: Partial<TEntity> }): Promise<Model<TEntity, TEntities>>
const post = await this.create({
data: { title: 'Hello', content: 'World', authorId: 1 },
});

createMany

createMany(options: { data: Partial<TEntity>[] }): Promise<Model<TEntity, TEntities>[]>
const posts = await this.createMany({
data: [
{ title: 'First', authorId: 1 },
{ title: 'Second', authorId: 2 },
],
});

update

update(options: {
where: Where<TEntity, TEntities>;
data: Partial<TEntity>;
}): Promise<Model<TEntity, TEntities>>
const updated = await this.update({
where: { id: 1 },
data: { published: true },
});

updateMany

updateMany(options: {
where: Where<TEntity, TEntities>;
data: Partial<TEntity>;
}): Promise<{ count: number }>
const { count } = await this.updateMany({
where: { authorId: 5 },
data: { published: false },
});

delete

delete(options: { where: Where<TEntity, TEntities> }): Promise<Model<TEntity, TEntities>>
const deleted = await this.delete({ where: { id: 1 } });

deleteMany

deleteMany(options: { where: Where<TEntity, TEntities> }): Promise<{ count: number }>
const { count } = await this.deleteMany({ where: { published: false } });

aggregate

aggregate<const TOptions extends AggregateOptions<TEntity, TEntities>>(
options: TOptions,
): Promise<AggregateResult<Model<TEntity, TEntities>, TOptions>[]>
const result = await this.aggregate({
groupBy: ['author.fullName'],
_count: true,
_sum: { 'author.postsCount': true },
});
// result[0].author.fullName -> string
// result[0]._count -> number
// result[0]._sum.author.postsCount -> number | null

Service defaults

Override protected methods to enforce business-level defaults. Applied automatically to every call — from controllers, cron jobs, other services:

getDefaultWhere

Combined with caller-provided where via AND (both conditions must match). Use for tenant isolation, RBAC, soft-delete filtering:

protected getDefaultWhere(
upstream?: Where<PostEntity, EM>,
): Where<PostEntity, EM> | undefined {
return { tenantId: this.getCurrentTenantId() };
}

getDefaultOrderBy

Fallback — used only when the caller doesn’t provide their own orderBy:

protected getDefaultOrderBy(
upstream?: OrderBy<PostEntity, EM> | OrderBy<PostEntity, EM>[],
): OrderBy<PostEntity, EM> | undefined {
return { field: 'createdAt', order: 'desc' };
}

getDefaultSelect

Fallback — used only when the caller doesn’t provide their own select:

protected getDefaultSelect(
upstream?: Select<PostEntity, EM>,
): Select<PostEntity, EM> | undefined {
return { id: true, title: true, published: true };
}

Cross-entity access

The r property gives typed access to all registered entities:

async getPostWithAuthor(id: number) {
const post = await this.findFirst({ where: { id } });
const author = await this.r.users.findFirst({
where: { id: post?.authorId },
select: { id: true, fullName: true },
});
return { post, author };
}

DI decorators

Three injection decorators for different levels of access:

// Full Relayer client with all entities
constructor(@InjectRelayer() r: RelayerInstance<EM>) {}
// Single entity repository (lower-level, no service defaults)
constructor(@InjectEntity(PostEntity) repo: EntityRepo<PostEntity, EM>) {}
// Auto-registered service without custom class
constructor(@InjectQueryService(PostEntity) service: RelayerService<PostEntity, EM>) {}

Full example

See the complete service example in the repository.