Flesh-out model on the backend
This commit is contained in:
@@ -1,23 +1,34 @@
|
|||||||
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, RepertoireId, RequestId, SessionId, Sha256, UserId } from "common";
|
import { AttachmentId, PieceId, RepertoireId, RequestId, SessionId, Sha256_Bin, Sha256_Hex, UserId } from "common";
|
||||||
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, RepertoireEntry } from "./database";
|
import * as Db from "./database";
|
||||||
|
import * as Model from "./model";
|
||||||
const tbranded = <T>() => t.Transform(t.String())
|
|
||||||
.Decode(Function.unsafeCoerce<string, T>)
|
|
||||||
.Encode(Function.unsafeCoerce<T, string>);
|
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
|
|
||||||
.use(swagger())
|
.use(swagger({
|
||||||
|
scalarConfig: {
|
||||||
|
authentication: {
|
||||||
|
securitySchemes: {
|
||||||
|
cookieAuth: {
|
||||||
|
type: "apiKey",
|
||||||
|
in: "cookie",
|
||||||
|
name: "sessionId",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
swaggerOptions: {
|
||||||
|
withCredentials: true,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
.use(cors({ origin: process.env.NODE_ENV === "production" ? false : "localhost:5173" }))
|
.use(cors({ origin: process.env.NODE_ENV === "production" ? false : "localhost:5173" }))
|
||||||
|
|
||||||
.decorate("db", await initDatabase(process.env.DB_PATH))
|
.decorate("db", await Db.initDatabase(process.env.DB_PATH))
|
||||||
|
|
||||||
.resolve(async ({ db, cookie }) => {
|
.resolve(async ({ db, cookie }) => {
|
||||||
await db
|
await db
|
||||||
@@ -43,7 +54,7 @@ const app = new Elysia()
|
|||||||
|
|
||||||
const user = await db
|
const user = await db
|
||||||
.selectFrom("User")
|
.selectFrom("User")
|
||||||
.selectAll()
|
.select(["userId", "username", "admin"])
|
||||||
.where("userId", "=", res.userId)
|
.where("userId", "=", res.userId)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
@@ -51,7 +62,13 @@ const app = new Elysia()
|
|||||||
return { user: null };
|
return { user: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user };
|
return {
|
||||||
|
user: {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
admin: user.admin !== 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
.onTransform(async ({ db, request, server }) => {
|
.onTransform(async ({ db, request, server }) => {
|
||||||
@@ -86,14 +103,15 @@ const app = new Elysia()
|
|||||||
.get("/me", ({ user }) => {
|
.get("/me", ({ user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return user;
|
||||||
userId: user.userId,
|
}, {
|
||||||
username: user.username,
|
response: {
|
||||||
admin: user.admin !== 0,
|
200: Model.User,
|
||||||
};
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
.post("/login", async ({ db, body: { username, password }, cookie }) => {
|
.post("/login", async ({ db, body: { username, password }, cookie }) => {
|
||||||
@@ -113,7 +131,7 @@ const app = new Elysia()
|
|||||||
return error("Unauthorized", "Invalid username or password");
|
return error("Unauthorized", "Invalid username or password");
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionId = generateSessionId();
|
const sessionId = Db.generateSessionId();
|
||||||
await db
|
await db
|
||||||
.insertInto("Session")
|
.insertInto("Session")
|
||||||
.values({ sessionId, userId: user.userId, expiresAt: sql`datetime('now', '+7 days')` })
|
.values({ sessionId, userId: user.userId, expiresAt: sql`datetime('now', '+7 days')` })
|
||||||
@@ -132,12 +150,17 @@ const app = new Elysia()
|
|||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
admin: user.admin !== 0,
|
admin: user.admin !== 0,
|
||||||
|
sessionId,
|
||||||
};
|
};
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
username: t.String({ minLength: 1 }),
|
username: t.String({ minLength: 1 }),
|
||||||
password: t.String({ minLength: 1 }),
|
password: t.String({ minLength: 1 }),
|
||||||
}),
|
}),
|
||||||
|
response: {
|
||||||
|
200: Model.User,
|
||||||
|
401: t.Literal("Invalid username or password"),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
.post("/logout", async ({ db, cookie, set }) => {
|
.post("/logout", async ({ db, cookie, set }) => {
|
||||||
@@ -156,6 +179,11 @@ const app = new Elysia()
|
|||||||
.deleteFrom("Session")
|
.deleteFrom("Session")
|
||||||
.where("sessionId", "=", SessionId(sessionId))
|
.where("sessionId", "=", SessionId(sessionId))
|
||||||
.execute();
|
.execute();
|
||||||
|
}, {
|
||||||
|
|
||||||
|
response: {
|
||||||
|
204: t.Void(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
.post("/change-password", async ({ db, body: { username, currentPassword, newPassword }, set }) => {
|
.post("/change-password", async ({ db, body: { username, currentPassword, newPassword }, set }) => {
|
||||||
@@ -195,14 +223,82 @@ const app = new Elysia()
|
|||||||
currentPassword: t.String({ minLength: 1 }),
|
currentPassword: t.String({ minLength: 1 }),
|
||||||
newPassword: t.String({ minLength: 1 }),
|
newPassword: t.String({ minLength: 1 }),
|
||||||
}),
|
}),
|
||||||
|
response: {
|
||||||
|
204: t.Void(),
|
||||||
|
401: t.Literal("Invalid username or password"),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// --- MARK: USER MANAGEMENT -------------------------------------------
|
// --- MARK: USER MANAGEMENT -------------------------------------------
|
||||||
|
|
||||||
|
.post("/user", async ({ db, body: { username, password, admin }, user }) => {
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.admin) {
|
||||||
|
return error("Forbidden", "Operation allowed only for admin users");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = UserId(Bun.randomUUIDv7());
|
||||||
|
|
||||||
|
const newUser = await db
|
||||||
|
.insertInto("User")
|
||||||
|
.values({
|
||||||
|
userId,
|
||||||
|
username,
|
||||||
|
password: await Bun.password.hash(password),
|
||||||
|
admin: admin ? 1 : 0,
|
||||||
|
})
|
||||||
|
.returningAll()
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: newUser.userId,
|
||||||
|
username: newUser.username,
|
||||||
|
admin: newUser.admin !== 0,
|
||||||
|
};
|
||||||
|
}, {
|
||||||
|
body: Model.User_Post,
|
||||||
|
response: {
|
||||||
|
200: Model.User,
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
403: t.Literal("Operation allowed only for admin users"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
.get("/user", async ({ db, query, user }) => {
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
let q = db
|
||||||
|
.selectFrom("User")
|
||||||
|
.select("userId")
|
||||||
|
.orderBy(["username", "userId"])
|
||||||
|
.offset(query.offset ?? 0)
|
||||||
|
.limit(query.limit ?? 100);
|
||||||
|
|
||||||
|
if (query.username !== undefined) {
|
||||||
|
q = q.where("username", "like", "%" + query.username + "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await q.execute();
|
||||||
|
return res.map(({ userId }) => userId);
|
||||||
|
}, {
|
||||||
|
query: Model.User_Query,
|
||||||
|
response: {
|
||||||
|
200: t.Array(Model.UserId),
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
.get("/user/:userId", async ({ db, params: { userId }, user }) => {
|
.get("/user/:userId", async ({ db, params: { userId }, user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await db
|
const res = await db
|
||||||
@@ -212,7 +308,7 @@ const app = new Elysia()
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
if (res === undefined) {
|
if (res === undefined) {
|
||||||
return error("Not Found");
|
return error("Not Found", undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -222,8 +318,89 @@ const app = new Elysia()
|
|||||||
};
|
};
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
userId: tbranded<UserId>(),
|
userId: Model.UserId,
|
||||||
}),
|
}),
|
||||||
|
response: {
|
||||||
|
200: Model.User,
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
404: t.Void(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
.patch("/user/:userId", async ({ db, body: { username, password, admin }, params: { userId }, user }) => {
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.admin) {
|
||||||
|
return error("Forbidden", "Operation allowed only for admin users");
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await db
|
||||||
|
.updateTable("User")
|
||||||
|
.set({
|
||||||
|
...(username !== undefined ? { username } : undefined),
|
||||||
|
...(password !== undefined ? { password: await Bun.password.hash(password) } : undefined),
|
||||||
|
...(admin !== undefined ? { admin: admin ? 1 : 0 } : undefined),
|
||||||
|
})
|
||||||
|
.where("userId", "=", userId)
|
||||||
|
.returning(["userId", "username", "admin"])
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
if (res === undefined) {
|
||||||
|
return error("Not Found", undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: res.userId,
|
||||||
|
username: res.username,
|
||||||
|
admin: res.admin !== 0,
|
||||||
|
};
|
||||||
|
}, {
|
||||||
|
body: Model.User_Patch,
|
||||||
|
params: t.Object({
|
||||||
|
userId: Model.UserId,
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: Model.User,
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
403: t.Literal("Operation allowed only for admin users"),
|
||||||
|
404: t.Void(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
.delete("/user/:userId", async ({ db, params: { userId }, set, user }) => {
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.admin) {
|
||||||
|
return error("Forbidden", "Operation allowed only for admin users");
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await db
|
||||||
|
.deleteFrom("User")
|
||||||
|
.where("userId", "=", userId)
|
||||||
|
.returningAll()
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (res.length === 0) {
|
||||||
|
return error("Not Found");
|
||||||
|
}
|
||||||
|
|
||||||
|
set.status = "No Content";
|
||||||
|
}, {
|
||||||
|
params: t.Object({
|
||||||
|
userId: Model.UserId,
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
204: t.Void(),
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
403: t.Literal("Operation allowed only for admin users"),
|
||||||
|
404: t.Void(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// --- MARK: PIECE CRUD ------------------------------------------------
|
// --- MARK: PIECE CRUD ------------------------------------------------
|
||||||
@@ -231,7 +408,7 @@ const app = new Elysia()
|
|||||||
.post("/piece", async ({ db, body: { name, composer, lyricist, arranger }, user }) => {
|
.post("/piece", async ({ db, body: { name, composer, lyricist, arranger }, user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
const pieceId = PieceId(Bun.randomUUIDv7());
|
const pieceId = PieceId(Bun.randomUUIDv7());
|
||||||
@@ -242,20 +419,22 @@ const app = new Elysia()
|
|||||||
.returningAll()
|
.returningAll()
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
return res;
|
return {
|
||||||
|
...res,
|
||||||
|
attachments: [],
|
||||||
|
};
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: Model.Piece_Post,
|
||||||
name: t.String({ minLength: 1 }),
|
response: {
|
||||||
composer: t.Nullable(t.String({ minLength: 1 })),
|
200: Model.Piece,
|
||||||
lyricist: t.Nullable(t.String({ minLength: 1 })),
|
401: t.Literal("Session invalid or expired"),
|
||||||
arranger: t.Nullable(t.String({ minLength: 1 })),
|
},
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
.get("/piece", async ({ db, query, user }) => {
|
.get("/piece", async ({ db, query, user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
let q = db
|
let q = db
|
||||||
@@ -280,18 +459,17 @@ const app = new Elysia()
|
|||||||
const res = await q.execute();
|
const res = await q.execute();
|
||||||
return res.map(({ pieceId }) => pieceId);
|
return res.map(({ pieceId }) => pieceId);
|
||||||
}, {
|
}, {
|
||||||
query: t.Object({
|
query: Model.Piece_Query,
|
||||||
name: t.Optional(t.String()),
|
response: {
|
||||||
author: t.Optional(t.String()),
|
200: t.Array(Model.PieceId),
|
||||||
offset: t.Optional(t.Integer({ minimum: 0 })),
|
401: t.Literal("Session invalid or expired"),
|
||||||
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
},
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
.get("/piece/:pieceId", async ({ db, params: { pieceId }, user }) => {
|
.get("/piece/:pieceId", async ({ db, params: { pieceId }, user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
const piece = await db
|
const piece = await db
|
||||||
@@ -301,7 +479,7 @@ const app = new Elysia()
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
if (piece === undefined) {
|
if (piece === undefined) {
|
||||||
return error("Not Found");
|
return error("Not Found", undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachments = await db
|
const attachments = await db
|
||||||
@@ -313,20 +491,25 @@ const app = new Elysia()
|
|||||||
return {
|
return {
|
||||||
...piece,
|
...piece,
|
||||||
attachments: attachments.map(({ sha256, ...rest }) => ({
|
attachments: attachments.map(({ sha256, ...rest }) => ({
|
||||||
sha256: Buffer.from(sha256).toString("hex"),
|
sha256: Sha256_Hex(Buffer.from(sha256).toString("hex")),
|
||||||
...rest,
|
...rest,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
pieceId: tbranded<PieceId>(),
|
pieceId: Model.PieceId,
|
||||||
}),
|
}),
|
||||||
|
response: {
|
||||||
|
200: Model.Piece,
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
404: t.Void(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
.put("/piece/:pieceId", async ({ db, body: { name, composer, lyricist, arranger }, params: { pieceId }, user }) => {
|
.put("/piece/:pieceId", async ({ db, body: { name, composer, lyricist, arranger }, params: { pieceId }, user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await db
|
const res = await db
|
||||||
@@ -337,10 +520,22 @@ const app = new Elysia()
|
|||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (res.length === 0) {
|
if (res.length === 0) {
|
||||||
return error("Not Found");
|
return error("Not Found", undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res[0];
|
const attachments = await db
|
||||||
|
.selectFrom("Attachment")
|
||||||
|
.selectAll()
|
||||||
|
.where("pieceId", "=", pieceId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...res[0],
|
||||||
|
attachments: attachments.map(({ sha256, ...rest }) => ({
|
||||||
|
sha256: Sha256_Hex(Buffer.from(sha256).toString("hex")),
|
||||||
|
...rest,
|
||||||
|
})),
|
||||||
|
};
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
name: t.String({ minLength: 1 }),
|
name: t.String({ minLength: 1 }),
|
||||||
@@ -349,14 +544,19 @@ const app = new Elysia()
|
|||||||
arranger: t.Nullable(t.String({ minLength: 1 })),
|
arranger: t.Nullable(t.String({ minLength: 1 })),
|
||||||
}),
|
}),
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
pieceId: tbranded<PieceId>(),
|
pieceId: Model.PieceId,
|
||||||
}),
|
}),
|
||||||
|
response: {
|
||||||
|
200: Model.Piece,
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
404: t.Void(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
.delete("/piece/:pieceId", async ({ db, params: { pieceId }, set, user }) => {
|
.delete("/piece/:pieceId", async ({ db, params: { pieceId }, set, user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await db
|
const res = await db
|
||||||
@@ -366,14 +566,19 @@ const app = new Elysia()
|
|||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (res.length === 0) {
|
if (res.length === 0) {
|
||||||
return error("Not Found");
|
return error("Not Found", undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
set.status = "No Content";
|
set.status = "No Content";
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
pieceId: tbranded<PieceId>(),
|
pieceId: Model.PieceId,
|
||||||
}),
|
}),
|
||||||
|
response: {
|
||||||
|
204: t.Void(),
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
404: t.Void(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// --- MARK: ATTACHMENT CRUD -------------------------------------------
|
// --- MARK: ATTACHMENT CRUD -------------------------------------------
|
||||||
@@ -381,13 +586,13 @@ const app = new Elysia()
|
|||||||
.post("/piece/:pieceId/attachment", async ({ db, body: { filename, mediaType, data }, params: { pieceId }, user }) => {
|
.post("/piece/:pieceId/attachment", async ({ db, body: { filename, mediaType, data }, params: { pieceId }, user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachmentId = AttachmentId(Bun.randomUUIDv7());
|
const attachmentId = AttachmentId(Bun.randomUUIDv7());
|
||||||
const dataArray = new Uint8Array(await data.arrayBuffer());
|
const dataArray = new Uint8Array(await data.arrayBuffer());
|
||||||
|
|
||||||
const sha256 = Sha256(new Uint8Array(Bun.SHA256.byteLength));
|
const sha256 = Sha256_Bin(new Uint8Array(Bun.SHA256.byteLength));
|
||||||
Bun.SHA256.hash(dataArray, sha256);
|
Bun.SHA256.hash(dataArray, sha256);
|
||||||
|
|
||||||
await db
|
await db
|
||||||
@@ -404,7 +609,7 @@ const app = new Elysia()
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...res,
|
...res,
|
||||||
sha256: Buffer.from(res.sha256).toString("hex"),
|
sha256: Sha256_Hex(Buffer.from(res.sha256).toString("hex")),
|
||||||
};
|
};
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
@@ -413,8 +618,12 @@ const app = new Elysia()
|
|||||||
data: t.File(),
|
data: t.File(),
|
||||||
}),
|
}),
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
pieceId: tbranded<PieceId>(),
|
pieceId: Model.PieceId,
|
||||||
}),
|
}),
|
||||||
|
response: {
|
||||||
|
200: Model.Attachment,
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
/* NOTE The piece ID is reduntant, because attachment IDs are unique for
|
/* NOTE The piece ID is reduntant, because attachment IDs are unique for
|
||||||
@@ -427,7 +636,7 @@ const app = new Elysia()
|
|||||||
.get("/piece/:pieceId/attachment/:attachmentId", async ({ db, params: { pieceId, attachmentId }, user, set }) => {
|
.get("/piece/:pieceId/attachment/:attachmentId", async ({ db, params: { pieceId, attachmentId }, user, set }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await db
|
const res = await db
|
||||||
@@ -441,7 +650,7 @@ const app = new Elysia()
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
if (res === undefined) {
|
if (res === undefined) {
|
||||||
return error("Not Found");
|
return error("Not Found", undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
set.headers["content-disposition"] = `attachment; filename*=UTF-8''${encodeURIComponent(res.filename)}`;
|
set.headers["content-disposition"] = `attachment; filename*=UTF-8''${encodeURIComponent(res.filename)}`;
|
||||||
@@ -449,15 +658,20 @@ const app = new Elysia()
|
|||||||
return new File([res.data], res.filename, { type: res.mediaType });
|
return new File([res.data], res.filename, { type: res.mediaType });
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
pieceId: tbranded<PieceId>(),
|
pieceId: Model.PieceId,
|
||||||
attachmentId: tbranded<AttachmentId>(),
|
attachmentId: Model.AttachmentId,
|
||||||
}),
|
}),
|
||||||
|
response: {
|
||||||
|
200: t.File(),
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
404: t.Void(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
.put("/piece/:pieceId/attachment/:attachmentId", async ({ db, body: { filename }, params: { pieceId, attachmentId }, user }) => {
|
.put("/piece/:pieceId/attachment/:attachmentId", async ({ db, body: { filename }, params: { pieceId, attachmentId }, user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await db
|
const res = await db
|
||||||
@@ -471,24 +685,32 @@ const app = new Elysia()
|
|||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (res.length === 0) {
|
if (res.length === 0) {
|
||||||
return error("Not Found");
|
return error("Not Found", undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res[0];
|
return {
|
||||||
|
...res[0],
|
||||||
|
sha256: Sha256_Hex(Buffer.from(res[0].sha256).toString("hex")),
|
||||||
|
};
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
filename: t.String({ minLength: 1 }),
|
filename: t.String({ minLength: 1 }),
|
||||||
}),
|
}),
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
pieceId: tbranded<PieceId>(),
|
pieceId: Model.PieceId,
|
||||||
attachmentId: tbranded<AttachmentId>(),
|
attachmentId: Model.AttachmentId,
|
||||||
}),
|
}),
|
||||||
|
response: {
|
||||||
|
200: Model.Attachment,
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
404: t.Void(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
.delete("/piece/:pieceId/attachment/:attachmentId", async ({ db, params: { pieceId, attachmentId }, set, user }) => {
|
.delete("/piece/:pieceId/attachment/:attachmentId", async ({ db, params: { pieceId, attachmentId }, set, user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await db
|
const res = await db
|
||||||
@@ -501,15 +723,20 @@ const app = new Elysia()
|
|||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (res.length === 0) {
|
if (res.length === 0) {
|
||||||
return error("Not Found");
|
return error("Not Found", undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
set.status = "No Content";
|
set.status = "No Content";
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
pieceId: tbranded<PieceId>(),
|
pieceId: Model.PieceId,
|
||||||
attachmentId: tbranded<AttachmentId>(),
|
attachmentId: Model.AttachmentId,
|
||||||
}),
|
}),
|
||||||
|
response: {
|
||||||
|
204: t.Void(),
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
404: t.Void(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// --- MARK: REPERTOIRE CRUD -------------------------------------------
|
// --- MARK: REPERTOIRE CRUD -------------------------------------------
|
||||||
@@ -517,7 +744,7 @@ const app = new Elysia()
|
|||||||
.post("/repertoire", async ({ db, body: { name, entries }, user }) => {
|
.post("/repertoire", async ({ db, body: { name, entries }, user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
const repertoireId = RepertoireId(Bun.randomUUIDv7());
|
const repertoireId = RepertoireId(Bun.randomUUIDv7());
|
||||||
@@ -528,7 +755,7 @@ const app = new Elysia()
|
|||||||
.returningAll()
|
.returningAll()
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
const dbEntries: RepertoireEntry[] = [];
|
const dbEntries: Db.RepertoireEntry[] = [];
|
||||||
for (let i = 0; i < entries.length; ++i) {
|
for (let i = 0; i < entries.length; ++i) {
|
||||||
const entry = entries[i];
|
const entry = entries[i];
|
||||||
const dbEntry = await db
|
const dbEntry = await db
|
||||||
@@ -547,15 +774,18 @@ const app = new Elysia()
|
|||||||
body: t.Object({
|
body: t.Object({
|
||||||
name: t.String({ minLength: 1 }),
|
name: t.String({ minLength: 1 }),
|
||||||
entries: t.Array(t.Object({
|
entries: t.Array(t.Object({
|
||||||
pieceId: tbranded<PieceId>(),
|
pieceId: Model.PieceId,
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
|
response: {
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
.get("/repertoire", async ({ db, query, user }) => {
|
.get("/repertoire", async ({ db, query, user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
let q = db
|
let q = db
|
||||||
@@ -577,12 +807,16 @@ const app = new Elysia()
|
|||||||
offset: t.Optional(t.Integer({ minimum: 0 })),
|
offset: t.Optional(t.Integer({ minimum: 0 })),
|
||||||
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
||||||
}),
|
}),
|
||||||
|
response: {
|
||||||
|
200: t.Array(Model.RepertoireId),
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
.get("/repertoire/:repertoireId", async ({ db, params: { repertoireId }, user }) => {
|
.get("/repertoire/:repertoireId", async ({ db, params: { repertoireId }, user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
const repertoire = await db
|
const repertoire = await db
|
||||||
@@ -592,30 +826,35 @@ const app = new Elysia()
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
if (repertoire === undefined) {
|
if (repertoire === undefined) {
|
||||||
return error("Not Found");
|
return error("Not Found", undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = await db
|
const entries = await db
|
||||||
.selectFrom("RepertoireEntry")
|
.selectFrom("RepertoireEntry")
|
||||||
.selectAll()
|
.select(["pieceId"])
|
||||||
.where("repertoireId", "=", repertoireId)
|
.where("repertoireId", "=", repertoireId)
|
||||||
.orderBy("order")
|
.orderBy("order")
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...repertoire,
|
...repertoire,
|
||||||
entries,
|
entries: entries.map(({ pieceId }) => pieceId),
|
||||||
};
|
};
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
repertoireId: tbranded<RepertoireId>(),
|
repertoireId: Model.RepertoireId,
|
||||||
}),
|
}),
|
||||||
|
response: {
|
||||||
|
200: Model.Repertoire,
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
404: t.Void(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
.put("/repertoire/:repertoireId", async ({ db, body: { name, entries }, params: { repertoireId }, user }) => {
|
.put("/repertoire/:repertoireId", async ({ db, body: { name, entries }, params: { repertoireId }, user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await db
|
const res = await db
|
||||||
@@ -626,7 +865,7 @@ const app = new Elysia()
|
|||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (res.length === 0) {
|
if (res.length === 0) {
|
||||||
return error("Not Found");
|
return error("Not Found", undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
@@ -634,37 +873,38 @@ const app = new Elysia()
|
|||||||
.where("repertoireId", "=", repertoireId)
|
.where("repertoireId", "=", repertoireId)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const dbEntries: RepertoireEntry[] = [];
|
|
||||||
for (let i = 0; i < entries.length; ++i) {
|
for (let i = 0; i < entries.length; ++i) {
|
||||||
const entry = entries[i];
|
const entry = entries[i];
|
||||||
const dbEntry = await db
|
await db
|
||||||
.insertInto("RepertoireEntry")
|
.insertInto("RepertoireEntry")
|
||||||
.values({ ...entry, repertoireId, order: i })
|
.values({ pieceId: entry, repertoireId, order: i })
|
||||||
.returningAll()
|
.returning(["pieceId"])
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
dbEntries.push(dbEntry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...res,
|
...res[0],
|
||||||
entries: dbEntries,
|
entries,
|
||||||
};
|
};
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
name: t.String({ minLength: 1 }),
|
name: t.String({ minLength: 1 }),
|
||||||
entries: t.Array(t.Object({
|
entries: t.Array(Model.PieceId),
|
||||||
pieceId: tbranded<PieceId>(),
|
|
||||||
})),
|
|
||||||
}),
|
}),
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
repertoireId: tbranded<RepertoireId>(),
|
repertoireId: Model.RepertoireId,
|
||||||
}),
|
}),
|
||||||
|
response: {
|
||||||
|
200: Model.Repertoire,
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
404: t.Void(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
.delete("/repertoire/:repertoireId", async ({ db, params: { repertoireId }, set, user }) => {
|
.delete("/repertoire/:repertoireId", async ({ db, params: { repertoireId }, set, user }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized", "Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await db
|
const res = await db
|
||||||
@@ -680,15 +920,20 @@ const app = new Elysia()
|
|||||||
set.status = "No Content";
|
set.status = "No Content";
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
repertoireId: tbranded<RepertoireId>(),
|
repertoireId: Model.RepertoireId,
|
||||||
}),
|
}),
|
||||||
// eslint-disable-next-line @stylistic/comma-dangle -- a comma would confuse the TS compiler here
|
response: {
|
||||||
|
204: t.Void(),
|
||||||
|
401: t.Literal("Session invalid or expired"),
|
||||||
|
404: t.Void(),
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @stylistic/comma-dangle -- a comma would confuse the TS compiler here
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
.get("*", () => Bun.file("packages/frontend/build/index.html"));
|
.get("*", () => Bun.file("packages/frontend/build/index.html"));
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
app.listen(process.env.PORT || 3000);
|
app.listen(process.env.PORT || 3000);
|
||||||
export type App = typeof app;
|
export type App = typeof app;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Database as BunSqliteDatabase } from "bun:sqlite";
|
import { Database as BunSqliteDatabase } from "bun:sqlite";
|
||||||
import { AttachmentId, PieceId, RepertoireId, RequestId, SessionId, Sha256, UserId } from "common";
|
import { AttachmentId, PieceId, RepertoireId, RequestId, SessionId, Sha256_Bin, 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";
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ export interface AccessLogTable {
|
|||||||
|
|
||||||
export interface AttachmentData {
|
export interface AttachmentData {
|
||||||
pieceId: ColumnType<PieceId, PieceId, never>;
|
pieceId: ColumnType<PieceId, PieceId, never>;
|
||||||
sha256: Sha256;
|
sha256: Sha256_Bin;
|
||||||
filename: string;
|
filename: string;
|
||||||
mediaType: string;
|
mediaType: string;
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ export interface AttachmentTable extends AttachmentData, SystemInformation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FileTable {
|
export interface FileTable {
|
||||||
sha256: ColumnType<Sha256, Sha256, never>;
|
sha256: ColumnType<Sha256_Bin, Sha256_Bin, never>;
|
||||||
data: ColumnType<Uint8Array, Uint8Array, never>;
|
data: ColumnType<Uint8Array, Uint8Array, never>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,9 +69,12 @@ export interface RepertoireTable extends SystemInformation {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RepertoireEntryTable {
|
interface RepertoireEntryData {
|
||||||
repertoireId: ColumnType<RepertoireId, RepertoireId, never>;
|
|
||||||
pieceId: ColumnType<PieceId, PieceId, never>;
|
pieceId: ColumnType<PieceId, PieceId, never>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RepertoireEntryTable extends RepertoireEntryData {
|
||||||
|
repertoireId: ColumnType<RepertoireId, RepertoireId, never>;
|
||||||
order: number;
|
order: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
109
packages/backend/src/model.ts
Normal file
109
packages/backend/src/model.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import * as Common from "common";
|
||||||
|
import * as Function from "common/Function";
|
||||||
|
import { t } from "elysia";
|
||||||
|
|
||||||
|
const brandedString = <T>() => t.Transform(t.String())
|
||||||
|
.Decode(Function.unsafeCoerce<string, T>)
|
||||||
|
.Encode(Function.unsafeCoerce<T, string>);
|
||||||
|
|
||||||
|
export const Sha256_Hex = brandedString<Common.Sha256_Hex>();
|
||||||
|
export const AttachmentId = brandedString<Common.AttachmentId>();
|
||||||
|
export const PieceId = brandedString<Common.PieceId>();
|
||||||
|
export const RepertoireId = brandedString<Common.RepertoireId>();
|
||||||
|
export const RequestId = brandedString<Common.RequestId>();
|
||||||
|
export const SessionId = brandedString<Common.SessionId>();
|
||||||
|
export const UserId = brandedString<Common.UserId>();
|
||||||
|
|
||||||
|
const SystemInformation = Object.freeze({
|
||||||
|
createdBy: t.Nullable(UserId),
|
||||||
|
createdAt: t.String(),
|
||||||
|
modifiedBy: t.Nullable(UserId),
|
||||||
|
modifiedAt: t.Nullable(t.String()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Pagination = Object.freeze({
|
||||||
|
offset: t.Optional(t.Integer({ minimum: 0 })),
|
||||||
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AccessLog = t.Object({
|
||||||
|
requestId: RequestId,
|
||||||
|
timestamp: t.String(),
|
||||||
|
method: t.String(),
|
||||||
|
pathname: t.String(),
|
||||||
|
query: t.String(),
|
||||||
|
ip: t.Nullable(t.String()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Attachment = t.Object({
|
||||||
|
attachmentId: AttachmentId,
|
||||||
|
pieceId: PieceId,
|
||||||
|
filename: t.String(),
|
||||||
|
sha256: Sha256_Hex,
|
||||||
|
mediaType: t.String(),
|
||||||
|
...SystemInformation,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Piece = t.Object({
|
||||||
|
pieceId: PieceId,
|
||||||
|
name: t.String({ minLength: 1 }),
|
||||||
|
composer: t.Nullable(t.String({ minLength: 1 })),
|
||||||
|
lyricist: t.Nullable(t.String({ minLength: 1 })),
|
||||||
|
arranger: t.Nullable(t.String({ minLength: 1 })),
|
||||||
|
attachments: t.Array(Attachment),
|
||||||
|
...SystemInformation,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Piece_Post = t.Object({
|
||||||
|
name: t.String({ minLength: 1 }),
|
||||||
|
composer: t.Nullable(t.String({ minLength: 1 })),
|
||||||
|
lyricist: t.Nullable(t.String({ minLength: 1 })),
|
||||||
|
arranger: t.Nullable(t.String({ minLength: 1 })),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Piece_Query = t.Object({
|
||||||
|
name: t.Optional(t.String()),
|
||||||
|
author: t.Optional(t.String()),
|
||||||
|
...Pagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Repertoire = t.Object({
|
||||||
|
repertoireId: RepertoireId,
|
||||||
|
name: t.String(),
|
||||||
|
entries: t.Array(PieceId),
|
||||||
|
...SystemInformation,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const User = t.Object({
|
||||||
|
userId: UserId,
|
||||||
|
username: t.String(),
|
||||||
|
admin: t.Boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const User_Patch = t.Object({
|
||||||
|
username: t.Optional(t.String()),
|
||||||
|
password: t.Optional(t.String({ minLength: 8 })),
|
||||||
|
admin: t.Optional(t.Boolean()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const User_Post = t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String({ minLength: 8 }),
|
||||||
|
admin: t.Boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const User_Query = t.Object({
|
||||||
|
username: t.Optional(t.String()),
|
||||||
|
...Pagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AccessLog = typeof AccessLog.static;
|
||||||
|
export type Attachment = typeof Attachment.static;
|
||||||
|
export type Piece = typeof Piece.static;
|
||||||
|
export type Piece_Post = typeof Piece_Post.static;
|
||||||
|
export type Piece_Query = typeof Piece_Query.static;
|
||||||
|
export type Repertoire = typeof Repertoire.static;
|
||||||
|
export type User = typeof User.static;
|
||||||
|
export type User_Patch = typeof User_Patch.static;
|
||||||
|
export type User_Post = typeof User_Post.static;
|
||||||
|
export type User_Query = typeof User_Query.static;
|
||||||
@@ -3,8 +3,11 @@ import * as Brand from "./Brand";
|
|||||||
export type UUID = Brand.Branded<string, "UUID">;
|
export type UUID = Brand.Branded<string, "UUID">;
|
||||||
export const UUID = Brand.nominal<UUID>();
|
export const UUID = Brand.nominal<UUID>();
|
||||||
|
|
||||||
export type Sha256 = Brand.Branded<Uint8Array, "Sha256">;
|
export type Sha256_Bin = Brand.Branded<Uint8Array, "Sha256">;
|
||||||
export const Sha256 = Brand.nominal<Sha256>();
|
export const Sha256_Bin = Brand.nominal<Sha256_Bin>();
|
||||||
|
|
||||||
|
export type Sha256_Hex = Brand.Branded<string, "Sha256">;
|
||||||
|
export const Sha256_Hex = Brand.nominal<Sha256_Hex>();
|
||||||
|
|
||||||
export type AttachmentId = Brand.Branded<UUID, "AttachmentId">;
|
export type AttachmentId = Brand.Branded<UUID, "AttachmentId">;
|
||||||
export const AttachmentId = Brand.nominal<AttachmentId>();
|
export const AttachmentId = Brand.nominal<AttachmentId>();
|
||||||
|
|||||||
@@ -87,25 +87,12 @@ export const denormalizePiece = ({
|
|||||||
Effect.flatMap(denormalizeSystemInformation),
|
Effect.flatMap(denormalizeSystemInformation),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const denormalizeRepertoireEntry = ({
|
|
||||||
pieceId,
|
|
||||||
...rest
|
|
||||||
}: Db.RepertoireEntry) => pipe(
|
|
||||||
Effect.all({
|
|
||||||
piece: Effect.uninterruptible(pieceCache.get(pieceId)),
|
|
||||||
}, { concurrency: "unbounded" }),
|
|
||||||
Effect.map((entry) => Object.freeze({
|
|
||||||
...rest,
|
|
||||||
...entry,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const denormalizeRepertoire = ({
|
export const denormalizeRepertoire = ({
|
||||||
entries,
|
entries,
|
||||||
...rest
|
...rest
|
||||||
}: Db.Repertoire & { entries: Db.RepertoireEntry[] }) => pipe(
|
}: Db.Repertoire & { entries: PieceId[] }) => pipe(
|
||||||
Effect.all({
|
Effect.all({
|
||||||
entries: Effect.all(entries.map(denormalizeRepertoireEntry), { concurrency: "unbounded" }),
|
entries: Effect.all(entries.map((entry) => Effect.uninterruptible(pieceCache.get(entry))), { concurrency: "unbounded" }),
|
||||||
}, { concurrency: "unbounded" }),
|
}, { concurrency: "unbounded" }),
|
||||||
Effect.map((repertoire) => Object.freeze({
|
Effect.map((repertoire) => Object.freeze({
|
||||||
...rest,
|
...rest,
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ function RepertoireRow(props: RepertoireRow.Props) {
|
|||||||
piecesParts.push(<em>Brak</em>);
|
piecesParts.push(<em>Brak</em>);
|
||||||
} else {
|
} else {
|
||||||
const CUTOFF = 10;
|
const CUTOFF = 10;
|
||||||
let text = repertoire.entries.slice(0, CUTOFF).map(({ piece: { name } }) => name).join(", ");
|
let text = repertoire.entries.slice(0, CUTOFF).map(({ name }) => name).join(", ");
|
||||||
if (repertoire.entries.length > CUTOFF) {
|
if (repertoire.entries.length > CUTOFF) {
|
||||||
text += ", …";
|
text += ", …";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user