3. Write the service
// backend/src/tasks/tasks.service.ts
import { Inject, Injectable } from '@nestjs/common';
import type { Knex } from 'knex';
import { KNEX_CONNECTION } from 'src/knex/knex.module';
import { TaskRow } from 'src/db/types';
import { paginate } from 'src/common/pagination';
@Injectable()
export class TasksService {
constructor(@Inject(KNEX_CONNECTION) private readonly db: Knex) {}
async findByUserPaginated(
userId: string,
offset: number,
limit: number,
): Promise<{ items: TaskRow[]; total: number }> {
// The count query carries the same filter as the list, minus paging.
return paginate<TaskRow>(
this.db<TaskRow>('tasks')
.where({ user_id: userId })
.orderBy('created_at', 'desc')
.offset(offset)
.limit(limit),
this.db('tasks').where({ user_id: userId }),
);
}
async findById(id: string): Promise<TaskRow | undefined> {
return this.db<TaskRow>('tasks').where({ id }).first();
}
async create(input: { title: string; userId: string }): Promise<TaskRow> {
const [row] = await this.db<TaskRow>('tasks')
.insert({ title: input.title, user_id: input.userId })
.returning('*');
return row;
}
async update(input: {
id: string;
title?: string;
completed?: boolean;
}): Promise<TaskRow | undefined> {
const { id, ...set } = input;
if (Object.keys(set).length === 0) return this.findById(id);
const [row] = await this.db<TaskRow>('tasks')
.where({ id })
.update({
title: set.title,
completed: set.completed,
updated_at: new Date(),
})
.returning('*');
return row;
}
async delete(id: string): Promise<boolean> {
const n = await this.db<TaskRow>('tasks').where({ id }).delete();
return n > 0;
}
}
Every list goes through paginate() from src/common/pagination, as ItemsService.findAllPaginated does: it runs both queries and coerces the count, which pg returns as a string.