Skip to main content

Repository

A Repository is a special Component that is responsible for retrieving data from a database.

It can be used in conjunction with the built-in SQL query builder to build complex queries.

Definition

import { Repository } from "@tymber/core";

interface TodoItem {
id: number;
title: string;
description: string;
dueDate: Date,
completed: boolean;
}

export class TodoRepository extends Repository<number, TodoItem> {
tableName = "todos";
}

Registration

import { type Module, type AppInit } from "@tymber/core";
import { TodoRepository } from "./repositories/TodoRepository";

export const MyModule: Module = {
name: "my-module",
version: "1.2.3",

init(app: AppInit) {
app.component(TodoRepository);
},
};

API

Public methods

findById(ctx, id)

Find a single entity (or undefined) by its ID:

const item = await todoRepository.findById(ctx, todoId);

deleteById(ctx, id)

Delete a single entity by its ID:

try {
await todoRepository.deleteById(ctx, todoId);
} catch (e) {
if (e instanceof EntityNotFoundError) {
// ...
} else {
throw e;
}
}

save(ctx, entity)

Upsert the entity:

const { id } = await todoRepository.save(ctx, todoItem);

The entity is converted to a row with the toRow() method.

startTransaction(ctx)

Start a new transaction:

await todoRepository.startTransaction(ctx, async () => {
// ...
});

If the callback throws an error, all changes are rolled back.

Protected methods

all(ctx, statement)

Return all entities matching the given statement:

interface Query {
page: number;
size: number;
sort:
| "label:asc"
| "label:desc";
}

export class TodoRepository extends Repository<number, TodoItem> {
tableName = "todos";

find(ctx: Context, query: Query) {
const sqlQuery = sql
.select()
.from(this.tableName)
.offset((query.page - 1) * query.size)
.limit(query.size);

switch (query.sort) {
case "label:asc":
sqlQuery.orderBy(["lower(label)"]);
break;
case "label:desc":
sqlQuery.orderBy(["lower(label) desc"]);
break;
}

return this.all(ctx, sqlQuery);
}
}

The statement argument is a SQL query built with Tymber built-in SQL query builder

Each row is converted to an entity with the toEntity() method.

one(ctx, statement)

Return one single entity (or undefined) matching the given statement:

export class TodoRepository extends Repository<number, TodoItem> {
tableName = "todos";

findOneByLabel(ctx: Context, label: string) {
const query = sql
.select()
.from(this.tableName)
.where({ label });

return this.one(ctx, query);
}
}

count(ctx, statement)

Return the number of entities matching the given statement:

export class TodoRepository extends Repository<number, TodoItem> {
tableName = "todos";

find(ctx: Context) {
const countQuery = sql
.select([sql.raw("COUNT(*) as count")])
.from(this.tableName);

return this.count(ctx, countQuery);
}
}

toRow(entity)

Convert an entity to a row. By default, each field is converted from camel case to snake case.

This method can be overridden to customize the conversion:

export class TodoRepository extends Repository<number, TodoItem> {
tableName = "todos";

override toRow(entity: Partial<TodoItem>) {
const row = super.toRow(entity);
// ...
return row;
}
}

toEntity(row)

Convert a row to an entity. By default, each field is converted from snake case to camel case.

This method can be overridden to customize the conversion:

export class TodoRepository extends Repository<number, TodoItem> {
tableName = "todos";

override toEntity(row: Record<string, any>) {
const entity = super.toEntity(row);
// ...
return entity;
}
}

db

A DB instance is automatically injected into each repository:

  • db.run(ctx, statement) runs a SQL query and returns the number of affected rows
  • db.query(ctx, statement) runs a SQL query and returns the result as an array of rows
export class TodoRepository extends Repository<number, TodoItem> {
tableName = "todos";

async deleteExpiredEntries(ctx: Context) {
const { affectedRows } = await this.db.run(
ctx,
sql
.deleteFrom(this.tableName)
.where(sql.raw("due_date < NOW() - INTERVAL '30 days'")),
);

return affectedRows;
}
}