Implement DB interface for Piece CRUD
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Schema as S } from "@effect/schema";
|
import { Schema as S } from "@effect/schema";
|
||||||
import { SessionId } from "common";
|
import { SessionId } from "common";
|
||||||
import { Login, Logout, Me } from "common/api";
|
import { CreatePiece, GetPieces, UpdatePiece, DeletePiece, Login, Logout, Me } from "common/api";
|
||||||
import { AccessLog, SessionData } from "common/db";
|
import { AccessLog, SessionData } from "common/db";
|
||||||
import { DateTime, Duration, Either as E, Effect, Match, Option as O, pipe } from "effect";
|
import { DateTime, Duration, Either as E, Effect, Match, Option as O, pipe } from "effect";
|
||||||
import { constant } from "effect/Function";
|
import { constant } from "effect/Function";
|
||||||
@@ -111,7 +111,15 @@ export const app = pipe(
|
|||||||
yield* db.createAccessLog(accessLog);
|
yield* db.createAccessLog(accessLog);
|
||||||
console.log(`${DateTime.toDateUtc(accessLog.timestamp).toISOString()} ${accessLog.requestId} ${accessLog.method} ${accessLog.pathname}?${new URLSearchParams(accessLog.query).toString()} ${accessLog.ip}`);
|
console.log(`${DateTime.toDateUtc(accessLog.timestamp).toISOString()} ${accessLog.requestId} ${accessLog.method} ${accessLog.pathname}?${new URLSearchParams(accessLog.query).toString()} ${accessLog.ip}`);
|
||||||
|
|
||||||
if (yield* match(Login)) {
|
if (yield* match(CreatePiece)) {
|
||||||
|
return new Response(null, { status: 501 });
|
||||||
|
} else if (yield* match(GetPieces)) {
|
||||||
|
return new Response(null, { status: 501 });
|
||||||
|
} else if (yield* match(UpdatePiece)) {
|
||||||
|
return new Response(null, { status: 501 });
|
||||||
|
} else if (yield* match(DeletePiece)) {
|
||||||
|
return new Response(null, { status: 501 });
|
||||||
|
} else if (yield* match(Login)) {
|
||||||
|
|
||||||
const body = yield* requestJson(Login.props.request.schema);
|
const body = yield* requestJson(Login.props.request.schema);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Schema as S } from "@effect/schema";
|
import { Schema as S } from "@effect/schema";
|
||||||
import { Database as SqliteDatabase } from "bun:sqlite";
|
import { Database as SqliteDatabase } from "bun:sqlite";
|
||||||
import { SessionId, UserId } from "common";
|
import { PieceId, SessionId, UserId } from "common";
|
||||||
import { AccessLog, SessionData, User } from "common/db";
|
import { AccessLog, Piece, SessionData, User } from "common/db";
|
||||||
import { Context, Effect, Layer, pipe } from "effect";
|
import { Context, Effect, Layer, pipe, Option as O } from "effect";
|
||||||
import { NoSuchElementException } from "effect/Cause";
|
import { NoSuchElementException } from "effect/Cause";
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
|
|
||||||
@@ -16,6 +16,11 @@ export function generateSessionId(byteLength: number = 12): SessionId {
|
|||||||
export interface DatabaseInterface {
|
export interface DatabaseInterface {
|
||||||
readonly createAccessLog: (accessLog: AccessLog) => Effect.Effect<void>;
|
readonly createAccessLog: (accessLog: AccessLog) => Effect.Effect<void>;
|
||||||
|
|
||||||
|
readonly createPiece: (name: string, composer: O.Option<string>, lyricist: O.Option<string>, arranger: O.Option<string>, user: UserId) => Effect.Effect<PieceId>;
|
||||||
|
readonly getPieces: Effect.Effect<readonly Piece[]>;
|
||||||
|
readonly updatePiece: (pieceId: PieceId, name: string, composer: O.Option<string>, lyricist: O.Option<string>, arranger: O.Option<string>, user: UserId) => Effect.Effect<void, NoSuchElementException>;
|
||||||
|
readonly deletePiece: (pieceId: PieceId) => Effect.Effect<void, NoSuchElementException>;
|
||||||
|
|
||||||
readonly getUserByUsername: (username: string) => Effect.Effect<User, NoSuchElementException>;
|
readonly getUserByUsername: (username: string) => Effect.Effect<User, NoSuchElementException>;
|
||||||
readonly getUserById: (userId: UserId) => Effect.Effect<User, NoSuchElementException>;
|
readonly getUserById: (userId: UserId) => Effect.Effect<User, NoSuchElementException>;
|
||||||
|
|
||||||
@@ -96,6 +101,21 @@ export const DatabaseLive = (filePath: string = "db.sqlite3") => Layer.effect(Da
|
|||||||
[timestamp: string, requestId: string, method: string, pathname: string, query: string, ip: string | null]
|
[timestamp: string, requestId: string, method: string, pathname: string, query: string, ip: string | null]
|
||||||
>("INSERT INTO AccessLog (timestamp, requestId, method, pathname, query, ip) VALUES (?, ?, ?, ?, ?, ?)");
|
>("INSERT INTO AccessLog (timestamp, requestId, method, pathname, query, ip) VALUES (?, ?, ?, ?, ?, ?)");
|
||||||
|
|
||||||
|
const createPiece = db.prepare<
|
||||||
|
never,
|
||||||
|
[pieceId: PieceId, name: string, composer: string | null, lyricist: string | null, arranger: string | null, user: UserId]
|
||||||
|
>("INSERT INTO Piece (pieceId, name, composer, lyricist, arranger, createdBy, createdAt, modifiedBy, modifiedAt) VALUES (?1, ?2, ?3, ?4, ?5, ?6, datetime(), ?6, datetime())");
|
||||||
|
const getPieces = db.prepare<
|
||||||
|
typeof Piece.Encoded,
|
||||||
|
[]
|
||||||
|
>("SELECT pieceId, name, composer, lyricist, arranger, createdBy, createdAt, modifiedBy, modifiedAt FROM Piece ORDER BY name, composer, arranger");
|
||||||
|
const updatePiece = db.prepare<
|
||||||
|
never,
|
||||||
|
[pieceId: PieceId, name: string, composer: string | null, lyricist: string | null, arranger: string | null, user: UserId]
|
||||||
|
>("UPDATE Piece SET name = ?2, composer = ?3, lyricist = ?4, arranger = ?5, modifiedBy = ?6, modifiedAt = datetime() WHERE pieceId = ?1");
|
||||||
|
const pieceExists = db.prepare<object, [pieceId: PieceId]>("SELECT 1 FROM Piece WHERE PieceId = ?");
|
||||||
|
const deletePiece = db.prepare<never, [pieceId: PieceId]>("DELETE FROM Piece WHERE pieceId = ?");
|
||||||
|
|
||||||
const getUserByUsername = db.prepare<
|
const getUserByUsername = db.prepare<
|
||||||
{ userId: string, username: string, password: string, admin: number },
|
{ userId: string, username: string, password: string, admin: number },
|
||||||
[username: string]
|
[username: string]
|
||||||
@@ -119,6 +139,29 @@ export const DatabaseLive = (filePath: string = "db.sqlite3") => Layer.effect(Da
|
|||||||
createAccessLog.run(timestamp, requestId, method, pathname, query, ip);
|
createAccessLog.run(timestamp, requestId, method, pathname, query, ip);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
createPiece: (name: string, composer: O.Option<string>, lyricist: O.Option<string>, arranger: O.Option<string>, user: UserId) => Effect.sync(() => {
|
||||||
|
const pieceId = PieceId.make(ulid());
|
||||||
|
createPiece.run(pieceId, name, O.getOrNull(composer), O.getOrNull(lyricist), O.getOrNull(arranger), user);
|
||||||
|
return pieceId;
|
||||||
|
}),
|
||||||
|
|
||||||
|
getPieces: Effect.sync(() => {
|
||||||
|
const res = getPieces.all();
|
||||||
|
const decoder = S.decodeSync(Piece);
|
||||||
|
return Object.freeze(res.map((encoded) => decoder(encoded)));
|
||||||
|
}),
|
||||||
|
|
||||||
|
updatePiece: (pieceId: PieceId, name: string, composer: O.Option<string>, lyricist: O.Option<string>, arranger: O.Option<string>, user: UserId) => Effect.suspend(() => {
|
||||||
|
updatePiece.run(pieceId, name, O.getOrNull(composer), O.getOrNull(lyricist), O.getOrNull(arranger), user);
|
||||||
|
const matched = pieceExists.get(pieceId) !== null;
|
||||||
|
return matched ? Effect.void : Effect.fail(new NoSuchElementException());
|
||||||
|
}),
|
||||||
|
|
||||||
|
deletePiece: (pieceId: PieceId) => Effect.suspend(() => {
|
||||||
|
const matched = deletePiece.run(pieceId).changes > 0;
|
||||||
|
return matched ? Effect.void : Effect.fail(new NoSuchElementException());
|
||||||
|
}),
|
||||||
|
|
||||||
getUserByUsername: (username) => Effect.suspend(() => {
|
getUserByUsername: (username) => Effect.suspend(() => {
|
||||||
const res = getUserByUsername.get(username);
|
const res = getUserByUsername.get(username);
|
||||||
if (res === null) return Effect.fail(new NoSuchElementException());
|
if (res === null) return Effect.fail(new NoSuchElementException());
|
||||||
|
|||||||
@@ -1,7 +1,48 @@
|
|||||||
import { Api } from "make-api";
|
import { Api } from "make-api";
|
||||||
import { Schema as S } from "@effect/schema";
|
import { Schema as S } from "@effect/schema";
|
||||||
import { UserId } from "common";
|
import { PieceId, UserId } from "common";
|
||||||
import { pipe } from "effect";
|
import { pipe } from "effect";
|
||||||
|
import { Piece } from "./db";
|
||||||
|
|
||||||
|
// --- PIECES ------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const CreatePiece = pipe(
|
||||||
|
Api.make("POST", "piece"),
|
||||||
|
Api.requestBodyJson(S.Struct({
|
||||||
|
name: S.NonEmptyString,
|
||||||
|
composer: S.Union(S.NonEmptyString, S.Null),
|
||||||
|
lyricist: S.Union(S.NonEmptyString, S.Null),
|
||||||
|
arranger: S.Union(S.NonEmptyString, S.Null),
|
||||||
|
})),
|
||||||
|
Api.responseBodyJson(201, Piece),
|
||||||
|
Api.responseBodyText(400, S.String),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const GetPieces = pipe(
|
||||||
|
Api.make("GET", "piece"),
|
||||||
|
Api.responseBodyJson(200, S.Array(Piece)),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdatePiece = pipe(
|
||||||
|
Api.make("PUT", "piece", ["pieceId", PieceId]),
|
||||||
|
Api.requestBodyJson(S.Struct({
|
||||||
|
name: S.NonEmptyString,
|
||||||
|
composer: S.Union(S.NonEmptyString, S.Null),
|
||||||
|
lyricist: S.Union(S.NonEmptyString, S.Null),
|
||||||
|
arranger: S.Union(S.NonEmptyString, S.Null),
|
||||||
|
})),
|
||||||
|
Api.responseBodyJson(200, Piece),
|
||||||
|
Api.responseBodyText(400, S.String),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DeletePiece = pipe(
|
||||||
|
Api.make("DELETE", "piece", ["pieceId", PieceId]),
|
||||||
|
Api.responseBodyNone(200),
|
||||||
|
Api.responseBodyText(400, S.String),
|
||||||
|
Api.responseBodyText(404, S.String),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- AUTHENTICATION ----------------------------------------------------------
|
||||||
|
|
||||||
export const Me = pipe(
|
export const Me = pipe(
|
||||||
Api.make("GET", "me"),
|
Api.make("GET", "me"),
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
export const container = style({
|
export const container = style({
|
||||||
width: 800,
|
width: 1000,
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
|
|
||||||
margin: "0 auto",
|
margin: "0 auto",
|
||||||
|
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "stretch",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const loading = style({
|
export const loading = style({
|
||||||
@@ -13,6 +17,70 @@ export const loading = style({
|
|||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const content = style({
|
export const header = style({
|
||||||
padding: 8,
|
padding: 8,
|
||||||
|
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "baseline",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const logout = style({
|
||||||
|
padding: 8,
|
||||||
|
|
||||||
|
backgroundColor: "#C0C0C0",
|
||||||
|
|
||||||
|
borderWidth: 2,
|
||||||
|
borderStyle: "solid",
|
||||||
|
borderTopColor: "#E0E0E0",
|
||||||
|
borderLeftColor: "#E0E0E0",
|
||||||
|
borderRightColor: "#404040",
|
||||||
|
borderBottomColor: "#404040",
|
||||||
|
|
||||||
|
borderRadius: 4,
|
||||||
|
|
||||||
|
cursor: "pointer",
|
||||||
|
|
||||||
|
"selectors": {
|
||||||
|
"&:focus": {
|
||||||
|
outlineWidth: 2,
|
||||||
|
outlineStyle: "solid",
|
||||||
|
outlineColor: "#8080FF",
|
||||||
|
|
||||||
|
"@media": {
|
||||||
|
"(prefers-color-scheme: dark)": {
|
||||||
|
outlineColor: "#C0C0FF",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"&:active": {
|
||||||
|
borderTopColor: "#404040",
|
||||||
|
borderLeftColor: "#404040",
|
||||||
|
borderRightColor: "#E0E0E0",
|
||||||
|
borderBottomColor: "#E0E0E0",
|
||||||
|
|
||||||
|
"@media": {
|
||||||
|
"(prefers-color-scheme: dark)": {
|
||||||
|
outlineColor: "#C0C0FF",
|
||||||
|
|
||||||
|
borderTopColor: "#202020",
|
||||||
|
borderLeftColor: "#202020",
|
||||||
|
borderRightColor: "#606060",
|
||||||
|
borderBottomColor: "#606060",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"@media": {
|
||||||
|
"(prefers-color-scheme: dark)": {
|
||||||
|
backgroundColor: "#404040",
|
||||||
|
|
||||||
|
borderTopColor: "#606060",
|
||||||
|
borderLeftColor: "#606060",
|
||||||
|
borderRightColor: "#202020",
|
||||||
|
borderBottomColor: "#202020",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,9 +71,9 @@ export const Home = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={style.container}>
|
<div class={style.container}>
|
||||||
<div class={style.content}>
|
<div class={style.header}>
|
||||||
Użytkownik: {user.username}
|
<div>Użytkownik: {user.username}</div>
|
||||||
<button type="button" onClick={onLogoutClick}>Wyloguj się</button>
|
<div><button class={style.logout} type="button" onClick={onLogoutClick}>Wyloguj się</button></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user