Database example
Try it
Start your own copy of this example:
npm create @setu-ts my-app -- --example database
cd my-app && npm install
npm start
The commands further down run inside a clone of the repository.
This app uses DatabasePlugin({ type: 'memory' }), so it has no database service prerequisite. Its
routes create, read, and update a note through the public repository interface. The smoke check also
opens a transaction, writes a row, throws, and confirms that the rollback leaves the row count
unchanged.
pnpm --dir apps/database run start
pnpm --dir apps/database run smoke
Read the example deep dive for the concept and source
walkthrough.
Read, in order
src/modules/notes/index.ts
import type { IRouterApi } from '@setu-ts/common';
import { registerNotesRoutes } from './notes.controller.ts';
/** Registers the notes feature. */
export function registerNotesModule(router: IRouterApi): void {
registerNotesRoutes(router);
}
src/modules/notes/notes.controller.ts
import type { IRouterApi } from '@setu-ts/common';
import { notFound } from '@setu-ts/exceptions';
import { validateBody } from '@setu-ts/validation-plugin';
import { CreateNoteBody, UpdateNoteBody } from './notes.schemas.ts';
import { notesRepository } from './notes.service.ts';
const createBody = validateBody(CreateNoteBody);
const updateBody = validateBody(UpdateNoteBody);
/** Registers the notes routes over the database repository. */
export function registerNotesRoutes(router: IRouterApi): void {
router.group('/notes', (routes) => {
routes.post('/', {
middleware: [createBody],
handler: async (ctx) => {
const note = await notesRepository(ctx.services).create(createBody.value(ctx));
return ctx.response.status(201).json(note);
},
});
routes.get('/:id', async (ctx) => {
const note = await notesRepository(ctx.services).findById(ctx.params.id);
if (note === null) throw notFound(`Note ${ctx.params.id} was not found.`);
return ctx.response.json(note);
});
routes.patch('/:id', {
middleware: [updateBody],
handler: async (ctx) => {
const notes = notesRepository(ctx.services);
if ((await notes.findById(ctx.params.id)) === null) {
throw notFound(`Note ${ctx.params.id} was not found.`);
}
return ctx.response.json(await notes.update(ctx.params.id, updateBody.value(ctx)));
},
});
});
}
src/modules/notes/notes.schemas.ts
import { z } from 'zod';
export const CreateNoteBody = z.object({ id: z.string().min(1), text: z.string().min(1) });
export type CreateNoteBody = z.infer<typeof CreateNoteBody>;
export const UpdateNoteBody = CreateNoteBody.pick({ text: true });
export type UpdateNoteBody = z.infer<typeof UpdateNoteBody>;
export type Note = CreateNoteBody;
src/modules/notes/notes.service.ts
import { CAPABILITIES } from '@setu-ts/common';
import type { IServiceRegistry } from '@setu-ts/common';
import type { IDatabaseService, IRepository } from '@setu-ts/database-plugin';
import type { Note } from './notes.schemas.ts';
/** The entity name every notes repository is addressed by. */
export const NOTES_ENTITY = 'notes';
/** Resolves the notes repository from the application's database capability. */
export function notesRepository(services: IServiceRegistry): IRepository<Note> {
return services.get<IDatabaseService>(CAPABILITIES.DATABASE).getRepository<Note>(NOTES_ENTITY);
}
smoke.ts
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { CAPABILITIES } from '@setu-ts/common';
import type { IDatabaseService } from '@setu-ts/database-plugin';
import type { IKernelApplication } from '@setu-ts/kernel';
import type { Note } from './src/modules/notes/notes.schemas.ts';
import { NOTES_ENTITY } from './src/modules/notes/notes.service.ts';
import { createApp } from './setu.config.ts';
describe('database example', () => {
let app: IKernelApplication;
before(async () => {
app = createApp();
await app.start();
});
after(async () => {
await app.stop();
});
it('reads back a note it wrote', async () => {
const created = await app.inject({
method: 'POST',
url: '/notes',
body: { id: 'first', text: 'Write this note' },
});
assert.equal(created.statusCode, 201);
const read = await app.inject({ method: 'GET', url: '/notes/first' });
assert.equal(read.statusCode, 200);
assert.equal(read.json<Note>().text, 'Write this note');
});
it('persists an update', async () => {
const updated = await app.inject({
method: 'PATCH',
url: '/notes/first',
body: { text: 'Read the updated note' },
});
assert.equal(updated.statusCode, 200);
const reread = await app.inject({ method: 'GET', url: '/notes/first' });
assert.equal(reread.json<Note>().text, 'Read the updated note');
});
it('answers 404 for a note that does not exist', async () => {
const read = await app.inject({ method: 'GET', url: '/notes/missing' });
const patched = await app.inject({
method: 'PATCH',
url: '/notes/missing',
body: { text: 'x' },
});
assert.equal(read.statusCode, 404);
assert.equal(patched.statusCode, 404);
});
it('discards the writes of a transaction that throws', async () => {
const database = app.services.get<IDatabaseService>(CAPABILITIES.DATABASE);
const notes = database.getRepository<Note>(NOTES_ENTITY);
const countBefore = await notes.count();
await assert.rejects(
database.transaction(async (unitOfWork) => {
await unitOfWork.getRepository<Note>(NOTES_ENTITY).create({ id: 'rolled-back', text: '-' });
throw new Error('roll back');
}),
/roll back/,
);
assert.equal(await notes.count(), countBefore);
});
});