JUMBO refactor, still work in progress

This commit is contained in:
2025-10-07 00:14:31 +02:00
parent 3694492e1a
commit dc0ec5c635
50 changed files with 4283 additions and 3698 deletions

View File

@@ -0,0 +1,486 @@
import { AttachmentId, PieceId, RepertoireId, Sha256 } from "common";
import api, { NotFound, Role, Unauthorized } from "common/the_api";
import { DateTime, Effect, HashSet, Option, pipe } from "effect";
import { sql } from "kysely";
import { implement } from "./api";
import * as Authentication from "./services/Authentication";
import * as Database from "./services/Database";
const READ_ACCESS = HashSet.make(Role.Editor, Role.Viewer);
const WRITE_ACCESS = HashSet.make(Role.Editor);
const requireReadAccess = pipe(
Authentication.Authentication,
Effect.flatMap(({ me }) => me),
Effect.flatMap((user) => HashSet.isSubset(user.roles, READ_ACCESS)
? Effect.succeed(user)
: Effect.fail(Unauthorized.make())
),
);
const requireWriteAccess = pipe(
Authentication.Authentication,
Effect.flatMap(({ me }) => me),
Effect.flatMap((user) => HashSet.isSubset(user.roles, WRITE_ACCESS)
? Effect.succeed(user)
: Effect.fail(Unauthorized.make())
),
);
export const handle = implement(api, {
// --- Authentication ---
me: () => pipe(
Authentication.Authentication,
Effect.flatMap(({ me }) => me),
),
logout: () => pipe(
Authentication.Authentication,
Effect.flatMap(({ logout }) => logout),
),
getUser: (userId) => pipe(
Authentication.Authentication,
Effect.flatMap(({ getUser }) => getUser(userId)),
),
// --- Piece CRUD ---
createPiece: (piece) => Effect.gen(function* () {
const { userId } = yield* requireWriteAccess;
const db = yield* Database.Database;
const res = yield* db
.insertInto("Piece")
.values({
pieceId: PieceId.make(Bun.randomUUIDv7()),
name: piece.name,
composer: Option.getOrNull(piece.composer),
lyricist: Option.getOrNull(piece.lyricist),
arranger: Option.getOrNull(piece.arranger),
createdBy: userId,
createdAt: sql`datetime()`,
})
.returningAll()
.$call(Database.executeTakeFirstOrDie);
return {
pieceId: res.pieceId,
name: res.name,
composer: Option.fromNullable(res.composer),
lyricist: Option.fromNullable(res.lyricist),
arranger: Option.fromNullable(res.arranger),
attachments: [],
createdBy: Option.fromNullable(res.createdBy),
createdAt: DateTime.unsafeMake(res.createdAt),
modifiedBy: Option.fromNullable(res.modifiedBy),
modifiedAt: pipe(
res.modifiedAt,
Option.fromNullable,
Option.map(DateTime.unsafeMake),
),
};
}),
getPiece: (pieceId) => Effect.gen(function* () {
yield* requireReadAccess;
const db = yield* Database.Database;
const piece = yield* db
.selectFrom("Piece")
.selectAll()
.where("pieceId", "=", pieceId)
.$call(Database.executeTakeFirst)
.pipe(Effect.mapError(() => NotFound.make()));
const attachments = yield* db
.selectFrom("Attachment")
.selectAll()
.where("pieceId", "=", pieceId)
.$call(Database.execute);
return {
pieceId: piece.pieceId,
name: piece.name,
composer: Option.fromNullable(piece.composer),
lyricist: Option.fromNullable(piece.lyricist),
arranger: Option.fromNullable(piece.arranger),
attachments: attachments.map(({
createdBy,
createdAt,
modifiedBy,
modifiedAt,
...rest
}) => ({
createdBy: Option.fromNullable(createdBy),
createdAt: DateTime.unsafeMake(createdAt),
modifiedBy: Option.fromNullable(modifiedBy),
modifiedAt: pipe(
modifiedAt,
Option.fromNullable,
Option.map(DateTime.unsafeMake),
),
...rest,
})),
createdBy: Option.fromNullable(piece.createdBy),
createdAt: DateTime.unsafeMake(piece.createdAt),
modifiedBy: Option.fromNullable(piece.modifiedBy),
modifiedAt: pipe(
piece.modifiedAt,
Option.fromNullable,
Option.map(DateTime.unsafeMake),
),
};
}),
queryPieces: ({ name, author, offset, limit }) => Effect.gen(function* () {
yield* requireReadAccess;
const db = yield* Database.Database;
let query = db
.selectFrom("Piece")
.select("pieceId")
.orderBy(["name", "composer", "arranger"])
.offset(offset)
.limit(limit);
query = Option.match(name, {
onNone: () => query,
onSome: (name) => query.where("name", "like", "%" + name + "%"),
});
query = Option.match(author, {
onNone: () => query,
onSome: (author) => query.where((eb) => eb.or([
eb("composer", "like", "%" + author + "%"),
eb("arranger", "like", "%" + author + "%"),
eb("lyricist", "like", "%" + author + "%"),
]))
});
const res = yield* query.$call(Database.execute);
return res.map(({ pieceId }) => pieceId);
}),
updatePiece: ({ pieceId, ...piece }) => Effect.gen(function* () {
const { userId } = yield* requireWriteAccess;
const db = yield* Database.Database;
const res = yield* db
.updateTable("Piece")
.set({
name: piece.name,
composer: Option.getOrUndefined(piece.composer),
lyricist: Option.getOrUndefined(piece.lyricist),
arranger: Option.getOrUndefined(piece.arranger),
modifiedBy: userId,
modifiedAt: sql`datetime()`,
})
.where("pieceId", "=", pieceId)
.returningAll()
.$call(Database.executeTakeFirst)
.pipe(Effect.mapError(() => NotFound.make()));
const attachments = yield* db
.selectFrom("Attachment")
.selectAll()
.where("pieceId", "=", pieceId)
.$call(Database.execute);
return {
pieceId: res.pieceId,
name: res.name,
composer: Option.fromNullable(res.composer),
lyricist: Option.fromNullable(res.lyricist),
arranger: Option.fromNullable(res.arranger),
attachments: attachments.map(({
createdBy,
createdAt,
modifiedBy,
modifiedAt,
...rest
}) => ({
createdBy: Option.fromNullable(createdBy),
createdAt: DateTime.unsafeMake(createdAt),
modifiedBy: Option.fromNullable(modifiedBy),
modifiedAt: pipe(
modifiedAt,
Option.fromNullable,
Option.map(DateTime.unsafeMake),
),
...rest,
})),
createdBy: Option.fromNullable(res.createdBy),
createdAt: DateTime.unsafeMake(res.createdAt),
modifiedBy: Option.fromNullable(res.modifiedBy),
modifiedAt: pipe(
res.modifiedAt,
Option.fromNullable,
Option.map(DateTime.unsafeMake),
),
};
}),
deletePiece: (pieceId) => Effect.gen(function* () {
yield* requireWriteAccess;
const db = yield* Database.Database;
yield* db
.deleteFrom("Piece")
.where("pieceId", "=", pieceId)
.returning("pieceId")
.$call(Database.executeTakeFirst)
.pipe(Effect.mapError(() => NotFound.make()));
}),
// --- Attachment CRUD ---
createAttachment: (attachment) => Effect.gen(function* () {
const { userId } = yield* requireWriteAccess;
const db = yield* Database.Database;
const sha256 = Sha256.make(new Uint8Array(Bun.SHA256.byteLength));
Bun.SHA256.hash(attachment.data, sha256);
yield* db
.insertInto("File")
.values({ sha256, data: attachment.data })
.onConflict((cb) => cb.column("sha256").doNothing())
.$call(Database.execute);
const res = yield* db
.insertInto("Attachment")
.values({
attachmentId: AttachmentId.make(Bun.randomUUIDv7()),
pieceId: attachment.pieceId,
filename: attachment.filename,
mediaType: attachment.mediaType,
sha256,
createdBy: userId,
createdAt: sql`datetime()`,
})
.returningAll()
.$call(Database.executeTakeFirstOrDie);
return {
attachmentId: res.attachmentId,
pieceId: res.pieceId,
filename: res.filename,
sha256: res.sha256,
mediaType: res.mediaType,
createdBy: Option.fromNullable(res.createdBy),
createdAt: DateTime.unsafeMake(res.createdAt),
modifiedBy: Option.fromNullable(res.modifiedBy),
modifiedAt: pipe(
res.modifiedAt,
Option.fromNullable,
Option.map(DateTime.unsafeMake),
),
};
}),
getAttachment: (attachmentId) => Effect.gen(function* () {
yield* requireReadAccess;
const db = yield* Database.Database;
const res = yield* db
.selectFrom("File")
.innerJoin("Attachment", "File.sha256", "Attachment.sha256")
.select(["Attachment.filename", "Attachment.mediaType", "File.data"])
.where("Attachment.attachmentId", "=", attachmentId)
.$call(Database.executeTakeFirst)
.pipe(Effect.mapError(() => NotFound.make()));
return res;
}),
updateAttachment: ({ attachmentId, ...attachment }) => Effect.gen(function* () {
const { userId } = yield* requireWriteAccess;
const db = yield* Database.Database;
const res = yield* db
.updateTable("Attachment")
.set({
...attachment,
modifiedBy: userId,
modifiedAt: sql`datetime()`,
})
.returningAll()
.$call(Database.executeTakeFirst)
.pipe(Effect.mapError(() => NotFound.make()));
return {
attachmentId: res.attachmentId,
pieceId: res.pieceId,
filename: res.filename,
sha256: res.sha256,
mediaType: res.mediaType,
createdBy: Option.fromNullable(res.createdBy),
createdAt: DateTime.unsafeMake(res.createdAt),
modifiedBy: Option.fromNullable(res.modifiedBy),
modifiedAt: pipe(
res.modifiedAt,
Option.fromNullable,
Option.map(DateTime.unsafeMake),
),
};
}),
deleteAttachment: (attachmentId) => Effect.gen(function* () {
yield* requireWriteAccess;
const db = yield* Database.Database;
yield* db
.deleteFrom("Attachment")
.where("attachmentId", "=", attachmentId)
.returning("attachmentId")
.$call(Database.executeTakeFirst)
.pipe(Effect.mapError(() => NotFound.make()));
}),
// --- Repertoire CRUD ---
createRepertoire: (repertoire) => Effect.gen(function* () {
const { userId } = yield* requireWriteAccess;
const db = yield* Database.Database;
const repertoireId = RepertoireId.make(Bun.randomUUIDv7());
const res = yield* db
.insertInto("Repertoire")
.values({
repertoireId,
name: repertoire.name,
createdBy: userId,
createdAt: sql`datetime()`,
})
.returningAll()
.$call(Database.executeTakeFirstOrDie);
const entries = yield* pipe(
repertoire.entries,
Effect.forEach((pieceId, i) => db
.insertInto("RepertoireEntry")
.values({ pieceId, repertoireId, order: i })
.returningAll()
.$call(Database.executeTakeFirstOrDie)
.pipe(Effect.map(({ pieceId }) => pieceId)),
),
);
return {
repertoireId: res.repertoireId,
name: res.name,
entries,
createdBy: Option.fromNullable(res.createdBy),
createdAt: DateTime.unsafeMake(res.createdAt),
modifiedBy: Option.fromNullable(res.modifiedBy),
modifiedAt: pipe(
res.modifiedAt,
Option.fromNullable,
Option.map(DateTime.unsafeMake),
),
};
}),
getRepertoire: (repertoireId) => Effect.gen(function* () {
yield* requireReadAccess;
const db = yield* Database.Database;
const repertoire = yield* db
.selectFrom("Repertoire")
.selectAll()
.where("repertoireId", "=", repertoireId)
.$call(Database.executeTakeFirst)
.pipe(Effect.mapError(() => NotFound.make()));
const entries = yield* db
.selectFrom("RepertoireEntry")
.select("pieceId")
.where("repertoireId", "=", repertoireId)
.orderBy("order")
.$call(Database.execute);
return {
repertoireId: repertoire.repertoireId,
name: repertoire.name,
entries: entries.map(({ pieceId }) => pieceId),
createdBy: Option.fromNullable(repertoire.createdBy),
createdAt: DateTime.unsafeMake(repertoire.createdAt),
modifiedBy: Option.fromNullable(repertoire.modifiedBy),
modifiedAt: pipe(
repertoire.modifiedAt,
Option.fromNullable,
Option.map(DateTime.unsafeMake),
),
};
}),
queryRepertoire: ({ name, offset, limit }) => Effect.gen(function* () {
yield* requireReadAccess;
const db = yield* Database.Database;
let query = db
.selectFrom("Repertoire")
.select("repertoireId")
.orderBy("name")
.offset(offset)
.limit(limit);
query = Option.match(name, {
onNone: () => query,
onSome: (name) => query.where("name", "like", "%" + name + "%"),
});
const res = yield* query.$call(Database.execute);
return res.map(({ repertoireId }) => repertoireId);
}),
updateRepertoire: ({ repertoireId, ...repertoire }) => Effect.gen(function* () {
const { userId } = yield* requireWriteAccess;
const db = yield* Database.Database;
const res = yield* db
.updateTable("Repertoire")
.set({
name: repertoire.name,
modifiedBy: userId,
modifiedAt: sql`datetime()`,
})
.where("repertoireId", "=", repertoireId)
.returningAll()
.$call(Database.executeTakeFirst)
.pipe(Effect.mapError(() => NotFound.make()));
yield* db
.deleteFrom("RepertoireEntry")
.where("repertoireId", "=", repertoireId)
.$call(Database.execute);
const entries = yield* pipe(
repertoire.entries,
Effect.forEach((pieceId, i) => db
.insertInto("RepertoireEntry")
.values({ pieceId, repertoireId, order: i })
.returningAll()
.$call(Database.executeTakeFirstOrDie)
.pipe(Effect.map(({ pieceId }) => pieceId)),
),
);
return {
repertoireId: res.repertoireId,
name: res.name,
entries,
createdBy: Option.fromNullable(res.createdBy),
createdAt: DateTime.unsafeMake(res.createdAt),
modifiedBy: Option.fromNullable(res.modifiedBy),
modifiedAt: pipe(
res.modifiedAt,
Option.fromNullable,
Option.map(DateTime.unsafeMake),
),
};
}),
deleteRepertoire: (repertoireId) => Effect.gen(function* () {
yield* requireWriteAccess;
const db = yield* Database.Database;
yield* db
.deleteFrom("Repertoire")
.where("repertoireId", "=", repertoireId)
.returning("repertoireId")
.$call(Database.executeTakeFirst)
.pipe(Effect.mapError(() => NotFound.make()));
}),
});