Implement repertoire REST API

This commit is contained in:
Szymon Nowakowski
2024-12-25 22:32:33 +01:00
parent 8ef3572e79
commit b3ec1b9463
3 changed files with 217 additions and 3 deletions

View File

@@ -1,11 +1,11 @@
import cors from "@elysiajs/cors"; import cors from "@elysiajs/cors";
import { staticPlugin } from "@elysiajs/static"; import { staticPlugin } from "@elysiajs/static";
import { swagger } from "@elysiajs/swagger"; import { swagger } from "@elysiajs/swagger";
import { AttachmentId, PieceId, RequestId, SessionId, Sha256, UserId } from "common"; import { AttachmentId, PieceId, RepertoireId, RequestId, SessionId, Sha256, UserId } from "common";
import * as Function from "common/Function"; import * as Function from "common/Function";
import { Elysia, error, t } from "elysia"; import { Elysia, error, t } from "elysia";
import { sql } from "kysely"; import { sql } from "kysely";
import { generateSessionId, initDatabase } from "./database"; import { generateSessionId, initDatabase, RepertoireEntry } from "./database";
const tbranded = <T>() => t.Transform(t.String()) const tbranded = <T>() => t.Transform(t.String())
.Decode(Function.unsafeCoerce<string, T>) .Decode(Function.unsafeCoerce<string, T>)
@@ -510,6 +510,178 @@ const app = new Elysia()
pieceId: tbranded<PieceId>(), pieceId: tbranded<PieceId>(),
attachmentId: tbranded<AttachmentId>(), attachmentId: tbranded<AttachmentId>(),
}), }),
})
// --- MARK: REPERTOIRE CRUD -------------------------------------------
.post("/repertoire", async ({ db, body: { name, entries }, user }) => {
if (user === null) {
return error("Unauthorized");
}
const repertoireId = RepertoireId(Bun.randomUUIDv7());
const repertoire = await db
.insertInto("Repertoire")
.values({ repertoireId, name, createdBy: user.userId, createdAt: sql`datetime()` })
.returningAll()
.executeTakeFirstOrThrow();
const dbEntries: RepertoireEntry[] = [];
for (let i = 0; i < entries.length; ++i) {
const entry = entries[i];
const dbEntry = await db
.insertInto("RepertoireEntry")
.values({ ...entry, repertoireId, order: i })
.returningAll()
.executeTakeFirstOrThrow();
dbEntries.push(dbEntry);
}
return {
...repertoire,
entries: dbEntries,
};
}, {
body: t.Object({
name: t.String({ minLength: 1 }),
entries: t.Array(t.Object({
pieceId: tbranded<PieceId>(),
})),
}),
})
.get("/repertoire", async ({ db, query, user }) => {
if (user === null) {
return error("Unauthorized");
}
let q = db
.selectFrom("Repertoire")
.select("repertoireId")
.orderBy(["name"])
.offset(query.offset ?? 0)
.limit(query.limit ?? 100);
if (query.name !== undefined) {
q = q.where("name", "like", "%" + query.name + "%");
}
const res = await q.execute();
return res.map(({ repertoireId }) => repertoireId);
}, {
query: t.Object({
name: t.Optional(t.String()),
offset: t.Optional(t.Integer({ minimum: 0 })),
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
}),
})
.get("/repertoire/:repertoireId", async ({ db, params: { repertoireId }, user }) => {
if (user === null) {
return error("Unauthorized");
}
const repertoire = await db
.selectFrom("Repertoire")
.selectAll()
.where("repertoireId", "=", repertoireId)
.executeTakeFirst();
if (repertoire === undefined) {
return error("Not Found");
}
const entries = await db
.selectFrom("RepertoireEntry")
.selectAll()
.where("repertoireId", "=", repertoireId)
.orderBy("order")
.execute();
return {
...repertoire,
entries,
};
}, {
params: t.Object({
repertoireId: tbranded<RepertoireId>(),
}),
})
.put("/repertoire/:repertoireId", async ({ db, body: { name, entries }, params: { repertoireId }, user }) => {
if (user === null) {
return error("Unauthorized");
}
const res = await db
.updateTable("Repertoire")
.set({ name, modifiedBy: user.userId, modifiedAt: sql`datetime()` })
.where("repertoireId", "=", repertoireId)
.returningAll()
.execute();
if (res.length === 0) {
return error("Not Found");
}
await db
.deleteFrom("RepertoireEntry")
.where("repertoireId", "=", repertoireId)
.execute();
const dbEntries: RepertoireEntry[] = [];
for (let i = 0; i < entries.length; ++i) {
const entry = entries[i];
const dbEntry = await db
.insertInto("RepertoireEntry")
.values({ ...entry, repertoireId, order: i })
.returningAll()
.executeTakeFirstOrThrow();
dbEntries.push(dbEntry);
}
return {
...res,
entries: dbEntries,
};
}, {
body: t.Object({
name: t.String({ minLength: 1 }),
entries: t.Array(t.Object({
pieceId: tbranded<PieceId>(),
})),
}),
params: t.Object({
repertoireId: tbranded<RepertoireId>(),
}),
})
.delete("/repertoire/:repertoireId", async ({ db, params: { repertoireId }, set, user }) => {
if (user === null) {
return error("Unauthorized");
}
const res = await db
.deleteFrom("Repertoire")
.where("repertoireId", "=", repertoireId)
.returningAll()
.execute();
if (res.length === 0) {
return error("Not Found");
}
set.status = "No Content";
}, {
params: t.Object({
repertoireId: tbranded<RepertoireId>(),
}),
// eslint-disable-next-line @stylistic/comma-dangle -- a comma would confuse the TS compiler here // eslint-disable-next-line @stylistic/comma-dangle -- a comma would confuse the TS compiler here
}) })
) )

View File

@@ -1,5 +1,5 @@
import { Database as BunSqliteDatabase } from "bun:sqlite"; import { Database as BunSqliteDatabase } from "bun:sqlite";
import { AttachmentId, PieceId, RequestId, SessionId, Sha256, UserId } from "common"; import { AttachmentId, PieceId, RepertoireId, RequestId, SessionId, Sha256, UserId } from "common";
import { ColumnType, CompiledQuery, CreateTableBuilder, Kysely, Selectable } from "kysely"; import { ColumnType, CompiledQuery, CreateTableBuilder, Kysely, Selectable } from "kysely";
import { BunSqliteDialect } from "kysely-bun-sqlite"; import { BunSqliteDialect } from "kysely-bun-sqlite";
@@ -15,6 +15,8 @@ export interface Database {
Attachment: AttachmentTable; Attachment: AttachmentTable;
File: FileTable; File: FileTable;
Piece: PieceTable; Piece: PieceTable;
Repertoire: RepertoireTable;
RepertoireEntry: RepertoireEntryTable;
Session: SessionTable; Session: SessionTable;
User: UserTable; User: UserTable;
} }
@@ -62,6 +64,17 @@ export interface PieceTable extends PieceData, SystemInformation {
pieceId: ColumnType<PieceId, PieceId, never>; pieceId: ColumnType<PieceId, PieceId, never>;
} }
export interface RepertoireTable extends SystemInformation {
repertoireId: ColumnType<RepertoireId, RepertoireId, never>;
name: string;
}
export interface RepertoireEntryTable {
repertoireId: ColumnType<RepertoireId, RepertoireId, never>;
pieceId: ColumnType<PieceId, PieceId, never>;
order: number;
}
export interface SessionData { export interface SessionData {
userId: UserId; userId: UserId;
} }
@@ -82,6 +95,8 @@ export type AccessLog = Selectable<AccessLogTable>;
export type Attachment = Selectable<AttachmentTable>; export type Attachment = Selectable<AttachmentTable>;
export type File = Selectable<FileTable>; export type File = Selectable<FileTable>;
export type Piece = Selectable<PieceTable>; export type Piece = Selectable<PieceTable>;
export type Repertoire = Selectable<RepertoireTable>;
export type RepertoireEntry = Selectable<RepertoireEntryTable>;
export type Session = Selectable<SessionTable>; export type Session = Selectable<SessionTable>;
export type User = Selectable<UserTable>; export type User = Selectable<UserTable>;
@@ -153,6 +168,30 @@ export async function initDatabase(filename: string = "db.sqlite3"): Promise<Kys
.columns(["name", "composer", "arranger"]) .columns(["name", "composer", "arranger"])
.execute(); .execute();
await db.schema
.createTable("Repertoire")
.ifNotExists()
.addColumn("repertoireId", "text", (c) => c.notNull().primaryKey())
.addColumn("name", "text", (c) => c.notNull())
.$call(systemInformation)
.execute();
await db.schema
.createIndex("Repertoire_name")
.ifNotExists()
.on("Repertoire")
.column("name")
.execute();
await db.schema
.createTable("RepertoireEntry")
.ifNotExists()
.addColumn("repertoireId", "text", (c) => c.notNull().references("Repertoire.repertoireId").onDelete("cascade").onUpdate("cascade"))
.addColumn("order", "integer", (c) => c.notNull())
.addColumn("pieceId", "text", (c) => c.notNull().references("Piece.pieceId").onDelete("restrict").onUpdate("restrict"))
.addPrimaryKeyConstraint("pk_RepertoireEntry", ["repertoireId", "order"])
.execute();
await db.schema await db.schema
.createTable("Session") .createTable("Session")
.ifNotExists() .ifNotExists()

View File

@@ -12,6 +12,9 @@ export const AttachmentId = Brand.nominal<AttachmentId>();
export type PieceId = Brand.Branded<UUID, "PieceId">; export type PieceId = Brand.Branded<UUID, "PieceId">;
export const PieceId = Brand.nominal<PieceId>(); export const PieceId = Brand.nominal<PieceId>();
export type RepertoireId = Brand.Branded<UUID, "RepertoireId">;
export const RepertoireId = Brand.nominal<RepertoireId>();
export type RequestId = Brand.Branded<UUID, "RequestId">; export type RequestId = Brand.Branded<UUID, "RequestId">;
export const RequestId = Brand.nominal<RequestId>(); export const RequestId = Brand.nominal<RequestId>();