Implement repertoire REST API
This commit is contained in:
@@ -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
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user