diff --git a/frontend/src/StoccaTreRoot/index.tsx b/frontend/src/StoccaTreRoot/index.tsx deleted file mode 100644 index 66d2a26..0000000 --- a/frontend/src/StoccaTreRoot/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { h, Rung } from "@djledda/ladder"; -import StoccaTreLogo from "@/assets/stocca-tre-logo.svg"; -import "./stocca-tre-root.scss"; - -export default class StoccaTreRoot extends Rung { - constructor() { - super({}); - } - - build(): Node { - return
-
-
- {"Stocca -
- -
-
; - } -} \ No newline at end of file diff --git a/frontend/src/global.scss b/frontend/src/global.scss index 2e52c1d..de3e6fd 100644 --- a/frontend/src/global.scss +++ b/frontend/src/global.scss @@ -1,7 +1,7 @@ :root { --bg-color: #e8e2e2; --red-deep: #aa0000; - --red-shallow: #ec563f; + --red-shallow: #dc8383; } html, body { diff --git a/frontend/src/main.ts b/frontend/src/main.ts index a1f794d..63085ad 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,5 +1,5 @@ import { bootstrap } from '@djledda/ladder'; -import StoccaTreRoot from './StoccaTreRoot'; +import StoccaTreRoot from './ui/StoccaTreRoot'; import './global.scss'; bootstrap(new StoccaTreRoot(), "root"); \ No newline at end of file diff --git a/frontend/src/ui/StoccaTreRoot/index.tsx b/frontend/src/ui/StoccaTreRoot/index.tsx new file mode 100644 index 0000000..6877810 --- /dev/null +++ b/frontend/src/ui/StoccaTreRoot/index.tsx @@ -0,0 +1,81 @@ +import {Capsule, h, Rung } from "@djledda/ladder"; +import StoccaTreLogo from "@/assets/stocca-tre-logo.svg"; +import "./stocca-tre-root.scss"; +import IngredientsPage from "../pages/IngredientsPage"; + +export default class StoccaTreRoot extends Rung { + private tabContainer = Capsule.new(null); + private tabs: HTMLLIElement[] = []; + private mainContent = Capsule.new(null); + + constructor() { + super({}); + } + + deselectTab(index: number) { + this.tabs[index]?.classList.remove("active"); + } + + selectTab(index: number) { + for (let i = 0; i < this.tabs.length; i++) { + if (i === index) { + this.tabs[i]?.classList.add("active"); + } else { + this.deselectTab(i); + } + } + if (index === 2) { + this.mainContent.val?.firstChild?.replaceWith(
{new IngredientsPage({})}
) + } else { + this.mainContent.val?.firstChild?.replaceWith(
); + } + } + + Tab = (props: { label: string }) => { + const index = this.tabs.length; + const tab =
  • { + this.selectTab(index); + }}> + { props.label } +
  • as HTMLLIElement; + this.tabs.push(tab); + return tab; + } + + returnToHome(): void { + this.tabs.forEach((tab, i) => this.deselectTab(i)); + this.mainContent.val?.firstChild?.replaceWith() + } + + Home = () => { + return
    Navigiere auf eine Unterseite!
    ; + } + + build(): Node { + this.tabs = []; + return
    + +
    +
    +
    + {/* Main Content */} + +
    +
    +
    +
    ; + } +} \ No newline at end of file diff --git a/frontend/src/StoccaTreRoot/stocca-tre-root.scss b/frontend/src/ui/StoccaTreRoot/stocca-tre-root.scss similarity index 74% rename from frontend/src/StoccaTreRoot/stocca-tre-root.scss rename to frontend/src/ui/StoccaTreRoot/stocca-tre-root.scss index 0ffec03..3c99e4b 100644 --- a/frontend/src/StoccaTreRoot/stocca-tre-root.scss +++ b/frontend/src/ui/StoccaTreRoot/stocca-tre-root.scss @@ -1,10 +1,12 @@ .stocca-tre-root { + width: 1200px; + margin: auto; + cursor: pointer; + .headstock { - width: 1200px; text-align: center; background-color: var(--bg-color); height: 100%; - margin: auto; .logo { margin: 20px auto auto auto; @@ -23,6 +25,10 @@ flex: 1; padding: 20px; display: inline-block; + &:hover, &.active { + background-color: var(--red-shallow); + cursor: pointer; + } } } } diff --git a/frontend/src/ui/pages/IngredientsPage/index.tsx b/frontend/src/ui/pages/IngredientsPage/index.tsx new file mode 100644 index 0000000..51ebbfa --- /dev/null +++ b/frontend/src/ui/pages/IngredientsPage/index.tsx @@ -0,0 +1,47 @@ +import {h, frag, Rung, RungOptions, Capsule} from "@djledda/ladder"; + +export default class IngredientsPage extends Rung { + private ingredients: any[] = []; + private list = Capsule.new(null); + private ingredientInput = Capsule.new(null); + + constructor(options: RungOptions) { + super({}); + } + + async addIngredient() { + await fetch("http://localhost:8080/ingredients/add", { + body: this.ingredientInput.val?.value ?? "{}", + method: "POST", + }); + this.ingredients.push(JSON.parse(this.ingredientInput.val?.value ?? "{}")); + this.refreshList(); + } + + async getIngredients() { + const result = await (await fetch("http://localhost:8080/ingredients/all")).json(); + this.ingredients = result.data.ingredients; + this.refreshList(); + } + + refreshList() { + this.list.val?.replaceWith(); + } + + List = (): HTMLUListElement => { + const thing =
    {...this.ingredients.map((ingredient) => { + return
    {ingredient.displayNameDE}, hinzugefügt von {`${ingredient.addedBy}`}
    ; + })}
    as HTMLUListElement; + return thing; + } + + build(): Node { + const node = <> + + + + ; + this.getIngredients(); + return node; + } +} \ No newline at end of file diff --git a/server/Maybe.ts b/server/Maybe.ts deleted file mode 100644 index 6a1ff74..0000000 --- a/server/Maybe.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type Maybe = Just | { just?: never; error: { message: string } }; -export type Just = { just: T; error?: never }; diff --git a/server/Result.ts b/server/Result.ts new file mode 100644 index 0000000..7d06fef --- /dev/null +++ b/server/Result.ts @@ -0,0 +1,35 @@ +export class StoccaTreError { + public message: string; + private statusCode: number = 500; + + constructor(message: string, status: number = 500) { + this.message = message; + this.statusCode = 500; + this.status = status; + } + + qualified(qualification: string): this { + const insertionPoint = qualification.indexOf("$err"); + this.message = insertionPoint !== -1 + ? `${qualification.slice(0, insertionPoint)}${this.message}${qualification.slice(insertionPoint + 4)}` + : `${qualification}${this.message}`; + return this; + } + + get status(): number { + return this.statusCode; + } + + set status(code: number) { + if (code >= 100 && code < 600) { + this.statusCode = code; + } + } + + withStatus(code: number): this { + this.status = code; + return this; + } +} + +export type Result = [error: undefined, just: T] | [error: StoccaTreError, just?: undefined]; diff --git a/server/StoccaTreRequest.ts b/server/StoccaTreRequest.ts index 6401dc3..dd77db0 100644 --- a/server/StoccaTreRequest.ts +++ b/server/StoccaTreRequest.ts @@ -1,22 +1,25 @@ +import Http = Deno.errors.Http; +import {JSONObject} from "./JSON.ts"; + export type HttpMethod = "POST" | "GET" | "PUT" | "DELETE"; export type RouteDefinition = { pattern: RegExp; - method: HttpMethod; + method?: HttpMethod; }; export default class StoccaTreRequest { constructor( public method: HttpMethod, public route: string, - public body: string | null, + public body: JSONObject | null, ) {} - match(route: RouteDefinition): RegExpExecArray | null { + match(route: RouteDefinition): RegExpExecArray | false { const patternResult = route.pattern.exec(this.route); - if (route.method !== this.method) { - return null; + if (route.method && route.method !== this.method) { + return false; } - return patternResult; + return patternResult ?? false; } } diff --git a/server/StoccaTreRequestHandler.ts b/server/StoccaTreRequestHandler.ts index 2e55595..bc7bfad 100644 --- a/server/StoccaTreRequestHandler.ts +++ b/server/StoccaTreRequestHandler.ts @@ -1,7 +1,7 @@ import StoccaTreRequest from "./StoccaTreRequest.ts"; -import { Maybe } from "./Maybe.ts"; +import { Result } from "./Result.ts"; import { JSONObject } from "./JSON.ts"; export default interface StoccaTreRequestHandler { - handleRequest(request: StoccaTreRequest): Promise>; + handleRequest(request: StoccaTreRequest): Promise | null>; } diff --git a/server/StoccaTreServer.ts b/server/StoccaTreServer.ts new file mode 100644 index 0000000..9e2eda5 --- /dev/null +++ b/server/StoccaTreServer.ts @@ -0,0 +1,35 @@ +import StoccaTreRequestHandler from "./StoccaTreRequestHandler.ts"; +import StoccaTreRequest, { RouteDefinition } from "./StoccaTreRequest.ts"; +import { Result, StoccaTreError } from "./Result.ts"; +import { JSONObject } from "./JSON.ts"; +import { StoccaTreDbConn } from "./database.ts"; + +export default class StoccaTreServer implements StoccaTreRequestHandler { + private db: StoccaTreDbConn; + private routes: { + routeDef: RouteDefinition; + handler: StoccaTreRequestHandler; + }[] = []; + + constructor(dbConnection: StoccaTreDbConn) { + this.db = dbConnection; + } + + addResource(route: RouteDefinition, handler: StoccaTreRequestHandler) { + this.routes.push({ routeDef: route, handler }); + } + + async handleRequest(request: StoccaTreRequest): Promise> { + let result: Result | null = null; + for (const { routeDef, handler } of this.routes) { + if (request.match(routeDef)) { + result = await handler.handleRequest(request); + break; + } + } + if (!result) { + return [new StoccaTreError(`Invalid route: ${request.route} with method ${request.method}.`, 400)]; + } + return result; + } +} diff --git a/server/config.ts b/server/config.ts index 96240e4..6be6603 100644 --- a/server/config.ts +++ b/server/config.ts @@ -1,13 +1,15 @@ const config = { - username: Deno.env.get("DB_USER") ?? "postgres", - hostname: Deno.env.get("DB_HOST") ?? "localhost", - password: Deno.env.get("DB_PW") ?? "", + dbUsername: Deno.env.get("DB_USER") ?? "postgres", + dbHostname: Deno.env.get("DB_HOST") ?? "localhost", + dbPassword: Deno.env.get("DB_PASS") ?? "", dbPort: Number(Deno.env.get("DB_PORT") ?? 5432), + hostname: Deno.env.get("HOST") ?? "localhost", port: Number(Deno.env.get("PORT") ?? 8080), }; console.log(`ENV: -db username: ${config.username} +db username: ${config.dbUsername} +db hostname: ${config.dbHostname} db pass: ****** db port: ${config.dbPort} server port: ${config.port} diff --git a/server/database.ts b/server/database.ts index 5636afd..2b92d97 100644 --- a/server/database.ts +++ b/server/database.ts @@ -1,42 +1,73 @@ import { Client } from "postgres"; import config from "./config.ts"; -import {Maybe} from "./Maybe.ts"; -import {JSONObject} from "./JSON.ts"; +import { JSONObject } from "./JSON.ts"; +import { Result, StoccaTreError } from "./Result.ts"; + +type Interpolable = number | string | bigint | null | boolean; +type UnionToIntersection = + (Union extends any ? (x: Union) => any : never) extends (x: infer Intersection) => any + ? Intersection + : never; +type SQLWithArg = Arg extends string ? `${string}$${Arg}${string}` : never; +type SQLWithArgs> = UnionToIntersection>; +type ArgNamesInSQL = + SQL extends `${string}$${infer Arg1} ${infer SQLAfterArg1}` + ? Arg1 extends `${infer Arg1NoTrailing}${";" | "," | "'" | "\"" | "(" | ")"}` + ? Arg1NoTrailing | ArgNamesInSQL + : Arg1 | ArgNamesInSQL + : SQL extends `${string}$${infer ArgN}` + ? ArgN extends `${infer ArgNNoTrailing}${";" | "," | "'" | "\"" | "(" | ")"}` + ? ArgNNoTrailing + : ArgN + : never; + +type ParametrisedSQL = `${string}$${string}`; +type IsPlainSQL = T extends ParametrisedSQL ? never : T; +type ArgsForSQL = SQL extends ParametrisedSQL + ? Record, Interpolable> + : JSONObject | undefined; + +// Helper for casting queries +export type Q = Result; export type WithoutId = Omit; export interface StoccaTreDbConn { - query(query: string): Promise>, + query(query: Q, ...args: Q extends IsPlainSQL ? [(JSONObject | undefined)?] : [ArgsForSQL]): Promise>; } -export default function createNewDbConnection(): StoccaTreDbConn { - const postgresClient = new Client({ - hostname: config.hostname, - password: config.password, - user: config.username, - database: "stocca_tre", - port: config.port, - }); - await postgresClient.connect(); - return { - async queryMany(query: string): Promise> { - try { - const result = await postgresClient.queryArray(query); - return { - just: { - rows: result.rows, - count: result.rowCount ?? NaN, - }, - }; - } catch (e: unknown) { - } - }, - queryOne(query: string): Promise> { - try { - const result = await postgresClient.queryObject(query); - return { just: result }; - } +export default async function createNewDbConnection(): Promise> { + let postgresClient: Client; + try { + postgresClient = new Client({ + user: config.dbUsername, + database: "stocca_tre", + hostname: config.hostname, + port: config.dbPort, + password: config.dbPassword, + tls: { + enabled: false, + }, + }); + await postgresClient.connect(); + } catch (e: unknown) { + const error = e as { message?: string }; + if (error.message) { + return [new StoccaTreError(error.message).qualified("Error connecting to database: ")]; } + return [new StoccaTreError("Error connecting to database.")]; } + return [, + { + async query(query: Q, ...args: Q extends IsPlainSQL ? [(JSONObject | undefined)?] : [ArgsForSQL]): Promise> { + try { + const result = <{rows: JSONObject[]}> await postgresClient.queryObject(query, args[0]); + return [, result.rows]; + } catch (e: unknown) { + const error = e as { message?: string }; + return [new StoccaTreError(error.message ?? "Internal database error.")]; + } + }, + }, + ]; } - diff --git a/server/main.ts b/server/main.ts index 3569f1f..40e0360 100644 --- a/server/main.ts +++ b/server/main.ts @@ -2,8 +2,9 @@ import createNewDbConnection, { StoccaTreDbConn } from "./database.ts"; import * as resources from "./resources/main.ts"; import config from "./config.ts"; import StoccaTreRequest, { HttpMethod } from "./StoccaTreRequest.ts"; -import StoccaTreRequestHandler from "./StoccaTreRequestHandler.ts"; import { JSONObject } from "./JSON.ts"; +import StoccaTreServer from "./StoccaTreServer.ts"; +import {Result, StoccaTreError} from "./Result.ts"; type StoccaTreApiBody = { data: JSONObject; @@ -16,44 +17,68 @@ function newStoccaTreApiBody(): StoccaTreApiBody { }; } -type StoccaTreServer = StoccaTreRequestHandler & { - dbConnection: StoccaTreDbConn; -}; +async function getJSONBody(request: Request): Promise> { + let json; + try { + if (request.body) { + json = await request.json(); + } else { + json = null; + } + } catch (e: unknown) { + return [new StoccaTreError((e as Error).message ?? "Body was invalid JSON.", 400)]; + } + return [, json]; +} -async function processRequest(server: StoccaTreServer, requestEvent: Deno.RequestEvent) { +async function respondWithError(requestEvent: Deno.RequestEvent, error: StoccaTreError) { + const body = newStoccaTreApiBody(); + await requestEvent.respondWith( + Response.json({ + ...body, + status: error.status, + error: error.qualified("Error: ").message, + }, { status: error.status }), + ); +} + +async function processRequest( + server: StoccaTreServer, + requestEvent: Deno.RequestEvent, +) { + const [bodyError, requestBody] = await getJSONBody(requestEvent.request); + if (bodyError) { + await respondWithError(requestEvent, bodyError); + return; + } const route = /^https?:\/\/[^\/]*(\/.*)$/.exec(requestEvent.request.url)?.[1] ?? "/"; - const requestBody = (await (await requestEvent.request.blob()).text()) ?? null; const method: HttpMethod = requestEvent.request.method as HttpMethod; const body: StoccaTreApiBody = newStoccaTreApiBody(); - const result = await server.handleRequest(new StoccaTreRequest(method, route, requestBody)); - if (result.error) { - await requestEvent.respondWith( - Response.json({ - ...body, - error: `Internal server error: ${result.error.message}`, - }, { status: 500 }), - ); - } else { - body.data = result.just; - await requestEvent.respondWith(Response.json(body, { status: 200 })); + const [error, result] = await server.handleRequest(new StoccaTreRequest(method, route, requestBody)); + if (error) { + await respondWithError(requestEvent, error); + return; + } + body.data = result; + await requestEvent.respondWith(Response.json(body, { status: 200 })); +} + + +const [error, database] = await createNewDbConnection(); + +if (error) { + console.log(error.qualified("Failed to create the database: ").message); +} else { + const server = new StoccaTreServer(database); + server.addResource({ pattern: /ingredient/ }, new resources.IngredientResource(database)); + server.addResource({ pattern: /user/ }, new resources.UserResource(database)); + const stoccaTreListener = Deno.listen({ port: config.port ?? 8080 }); + console.log(`Stocca Tre Server is running. Access it at: http://${ config.hostname }:${ config.port }/`); + for await (const conn of stoccaTreListener) { + const httpConn = Deno.serveHttp(conn); + for await (const requestEvent of httpConn) { + await processRequest(server, requestEvent); + } } } -const stoccaTreListener = Deno.listen({ port: config.port ?? 8080 }); - -console.log(`Stocca Tre Server is running. Access it at: http://localhost:${config.port}/`); - -const database = createNewDbConnection(); -const ingredientResource = new resources.IngredientResource(database); -const userResource = new resources.UserResource(database); -const stoccaTreServer: StoccaTreServer = { - dbConnection: database, - handleRequest: (request: StoccaTreRequest) => ingredientResource.handleRequest(request), -}; - -for await (const conn of stoccaTreListener) { - const httpConn = Deno.serveHttp(conn); - for await (const requestEvent of httpConn) { - await processRequest(stoccaTreServer, requestEvent); - } -} diff --git a/server/resources/ingredient/IngredientCollection.ts b/server/resources/ingredient/IngredientCollection.ts index c7d4a89..12b689f 100644 --- a/server/resources/ingredient/IngredientCollection.ts +++ b/server/resources/ingredient/IngredientCollection.ts @@ -1,6 +1,8 @@ -import { StoccaTreDbConn, WithoutId } from "../../database.ts"; +import { Q, StoccaTreDbConn, WithoutId } from "../../database.ts"; import { IngredientModel } from "./IngredientModel.ts"; -import { Maybe } from "../../Maybe.ts"; +import { Result, StoccaTreError } from "../../Result.ts"; + +const TABLE_NAME = "main.ingredients"; export default class IngredientCollection { private db: StoccaTreDbConn; @@ -10,36 +12,38 @@ export default class IngredientCollection { this.db = database; } - async addIngredient(ingredient: WithoutId): Promise> { - return await this.db.query(sql => - sql`INSERT INTO ingredients ${ sql(ingredient) }` - ); + async addIngredient(ingredient: WithoutId): Promise> { + const [error, result] = > await this.db.query(`INSERT INTO ${ TABLE_NAME } VALUES (DEFAULT, $name, $displayName, $displayNameDE) RETURNING *;`, ingredient); + if (error) { + return [error]; + } + if (result.length > 0) { + return [, result[0]]; + } + return [new StoccaTreError("Ingredient wasn't inserted!")]; } - async getById(id: number): Promise> { + async getById(id: number): Promise> { const found = this.mapById.get(id); if (found) { - return { just: found }; + return [, found]; } - const result = await this.db.query(sql => sql`SELECT * FROM ingredients WHERE id is ${ id }`); - if (result.error) { - return result; + const [error, result] = > await this.db.query(`SELECT * FROM ${ TABLE_NAME } WHERE id is $id`, { id }); + if (error) { + return [error]; } - const ingredient = result.just[0]; + const ingredient = result[0]; this.mapById.set(ingredient.id, ingredient); - return { just: ingredient }; + return [, ingredient]; } - async getAllIngredients(): Promise>> { - const result: Maybe = await this.db.query(sql => - sql`SELECT * FROM ingredients` - ); - if (!result.error) { - result.just.forEach(ingredient => this.mapById.set(ingredient.id, ingredient)); + + async getAllIngredients(): Promise>> { + const [error, result] = > await this.db.query(`SELECT * FROM ${ TABLE_NAME }`); + if (!error) { + result.forEach((ingredient: IngredientModel) => this.mapById.set(ingredient.id, ingredient)); } else { - return result; + return [error]; } - return { - just: this.mapById.values(), - }; + return [, this.mapById.values()]; } } diff --git a/server/resources/ingredient/IngredientModel.ts b/server/resources/ingredient/IngredientModel.ts index 69b9a4d..844b8e3 100644 --- a/server/resources/ingredient/IngredientModel.ts +++ b/server/resources/ingredient/IngredientModel.ts @@ -9,4 +9,6 @@ export const IngredientSchema = z.object({ export const IngredientSchemaWithoutId = IngredientSchema.omit({ id: true }); -export type IngredientModel = z.infer; \ No newline at end of file + +export type IngredientModel = z.infer; + diff --git a/server/resources/ingredient/IngredientResource.ts b/server/resources/ingredient/IngredientResource.ts index 109bc07..f9e97af 100644 --- a/server/resources/ingredient/IngredientResource.ts +++ b/server/resources/ingredient/IngredientResource.ts @@ -1,9 +1,9 @@ -import {StoccaTreDbConn} from "../../database.ts"; +import { StoccaTreDbConn } from "../../database.ts"; import IngredientCollection from "./IngredientCollection.ts"; -import StoccaTreRequest, {RouteDefinition} from "../../StoccaTreRequest.ts"; -import {Maybe} from "../../Maybe.ts"; -import {JSONObject} from "../../JSON.ts"; -import {IngredientSchemaWithoutId} from "./IngredientModel.ts"; +import StoccaTreRequest, { RouteDefinition } from "../../StoccaTreRequest.ts"; +import {Result, StoccaTreError} from "../../Result.ts"; +import { JSONObject } from "../../JSON.ts"; +import { IngredientModel, IngredientSchemaWithoutId } from "./IngredientModel.ts"; export default class IngredientResource { private dbConnection: StoccaTreDbConn; @@ -11,11 +11,11 @@ export default class IngredientResource { private routes: Readonly> = { Add: { pattern: /\/add/, - method: "POST" + method: "POST", }, GetAll: { pattern: /\/all/, - method: "GET" + method: "GET", }, } as const; @@ -24,33 +24,40 @@ export default class IngredientResource { this.collection = new IngredientCollection(dbConnection); } - async handleRequest(request: StoccaTreRequest): Promise> { + async handleRequest(request: StoccaTreRequest): Promise | null> { + let result; if (request.match(this.routes.Add)) { - return await this.addIngredient(request); + result = await this.addIngredient(request); } if (request.match(this.routes.GetAll)) { - return await this.allIngredients(request); + result = await this.allIngredients(request); } - return { error: { message: "Invalid route" }}; + if (result) { + const [error] = result; + if (error) { + return [error.qualified("Could not fulfill ingredient request: ")]; + } else { + return result; + } + } + return null; } - private async addIngredient(request: StoccaTreRequest): Promise> { - const ingredient = IngredientSchemaWithoutId.safeParse(JSON.parse(request.body ?? "{}")); + private async addIngredient(request: StoccaTreRequest): Promise> { + const ingredient = IngredientSchemaWithoutId.safeParse(request.body); if (!ingredient.success) { - return { error: new Error("Ingredient was malformed.") }; + return [new StoccaTreError("Ingredient definition was malformed.", 400)]; } return await this.collection.addIngredient(ingredient.data); } - private async allIngredients(request: StoccaTreRequest): Promise> { - const getAllIngredientResult = await this.collection.getAllIngredients(); - if (getAllIngredientResult.error) { - return getAllIngredientResult; + private async allIngredients(request: StoccaTreRequest): Promise> { + const [error, ingredients] = await this.collection.getAllIngredients(); + if (error) { + return [error]; } - return { - just: { - ingredients: Array.from(getAllIngredientResult.just), - }, - }; + return [, { + ingredients: Array.from(ingredients), + }]; } -} \ No newline at end of file +} diff --git a/server/resources/user/UserCollection.ts b/server/resources/user/UserCollection.ts index 0422050..deb36f8 100644 --- a/server/resources/user/UserCollection.ts +++ b/server/resources/user/UserCollection.ts @@ -1,7 +1,9 @@ import * as bcrypt from "bcrypt"; -import { StoccaTreDbConn, WithoutId } from "../../database.ts"; +import {Q, StoccaTreDbConn, WithoutId} from "../../database.ts"; import { UserModel } from "./UserModel.ts"; -import { Maybe } from "../../Maybe.ts"; +import {Result, StoccaTreError} from "../../Result.ts"; + +const TABLE_NAME = "main.users"; export default class UserCollection { private db: StoccaTreDbConn; @@ -11,40 +13,34 @@ export default class UserCollection { this.db = database; } - async addUser(user: WithoutId): Promise> { + async addUser(user: WithoutId): Promise> { let hash: string; try { hash = await bcrypt.hash(user.password); } catch (e: unknown) { const error = e as { message?: string }; if (typeof error.message === "string") { - return { error: { message: error.message }}; + return [new StoccaTreError(error.message)]; } - return { error: new Error("Failed to create user") }; + return [new StoccaTreError("Failed to create user")]; } user.password = hash; - const result: Maybe = await this.db.query(sql => sql` - INSERT INTO users ${ sql(user, "displayName") } - RETURNING * - `); - if (result.error) { - return result; + const [error, users] = > await this.db.query(`INSERT INTO ${TABLE_NAME} ($displayName, $password) RETURNING *`, user); + if (error) { + return [error]; } - return { - just: { insertedId: result.just[0]?.id ?? NaN } + if (users.length === 0) { + return [new StoccaTreError("Failed to insert user.")]; } - + return [, users[0]]; } - async getAllUsers(): Promise> { - const result = await this.db.query((sql) => sql`SELECT * FROM users`); - if (!result.error) { - result.just.forEach(user => this.mapById.set(user.id, user)); - } else { - return result; + async getAllUsers(): Promise> { + const [error, users] = > await this.db.query(`SELECT * FROM ${TABLE_NAME}`); + if (error) { + return [error]; } - return { - just: Array.from(this.mapById.values()), - }; + users.forEach((user) => this.mapById.set(user.id, user)); + return [, Array.from(this.mapById.values())]; } } diff --git a/server/resources/user/UserModel.ts b/server/resources/user/UserModel.ts index dda2dce..5a9c238 100644 --- a/server/resources/user/UserModel.ts +++ b/server/resources/user/UserModel.ts @@ -9,4 +9,4 @@ export const UserSchema = z.object({ export const UserSchemaWithoutId = UserSchema.omit({ id: true }); -export type UserModel = z.infer; \ No newline at end of file +export type UserModel = z.infer; diff --git a/server/resources/user/UserResource.ts b/server/resources/user/UserResource.ts index 0e13427..7617820 100644 --- a/server/resources/user/UserResource.ts +++ b/server/resources/user/UserResource.ts @@ -1,9 +1,9 @@ -import {StoccaTreDbConn} from "../../database.ts"; -import StoccaTreRequest, {RouteDefinition} from "../../StoccaTreRequest.ts"; -import {Maybe} from "../../Maybe.ts"; -import {JSONObject} from "../../JSON.ts"; +import { StoccaTreDbConn } from "../../database.ts"; +import StoccaTreRequest, { RouteDefinition } from "../../StoccaTreRequest.ts"; +import {Result, StoccaTreError} from "../../Result.ts"; +import { JSONObject } from "../../JSON.ts"; import UserCollection from "./UserCollection.ts"; -import {UserSchemaWithoutId} from "./UserModel.ts"; +import { UserSchemaWithoutId } from "./UserModel.ts"; export default class UserResource { private dbConnection: StoccaTreDbConn; @@ -11,11 +11,11 @@ export default class UserResource { private routes: Readonly> = { Add: { pattern: /\/add/, - method: "POST" + method: "POST", }, GetAll: { pattern: /\/all/, - method: "GET" + method: "GET", }, } as const; @@ -24,33 +24,29 @@ export default class UserResource { this.collection = new UserCollection(dbConnection); } - async handleRequest(request: StoccaTreRequest): Promise> { + async handleRequest(request: StoccaTreRequest): Promise | null> { if (request.match(this.routes.Add)) { return await this.addUser(request); } if (request.match(this.routes.GetAll)) { return await this.allUsers(request); } - return { error: { message: "Invalid route" }}; + return null; } - private async addUser(request: StoccaTreRequest): Promise> { - const user = UserSchemaWithoutId.safeParse(JSON.parse(request.body ?? "{}")); + private async addUser(request: StoccaTreRequest): Promise> { + const user = UserSchemaWithoutId.safeParse(request.body); if (!user.success) { - return { error: new Error("Ingredient was malformed.") }; + return [new StoccaTreError("User definition was malformed.", 400)]; } return await this.collection.addUser(user.data); } - private async allUsers(request: StoccaTreRequest): Promise> { - const allUsers = await this.collection.getAllUsers(); - if (allUsers.error) { - return allUsers; + private async allUsers(request: StoccaTreRequest): Promise> { + const [error, result] = await this.collection.getAllUsers(); + if (error) { + return [error]; } - return { - just: { - ingredients: Array.from(allUsers.just), - }, - }; + return [, { users: result }]; } } diff --git a/server/resources/user/main.ts b/server/resources/user/main.ts index 93a4084..3f50a87 100644 --- a/server/resources/user/main.ts +++ b/server/resources/user/main.ts @@ -1,3 +1,3 @@ export * from "./UserModel.ts"; -export * from "./UserCollection.ts"; -export * from "./UserResource.ts"; +export { default as UserCollection } from "./UserCollection.ts"; +export { default as UserResource } from "./UserResource.ts"; diff --git a/server/start.sh b/server/start.sh index 863833e..8283cf0 100644 --- a/server/start.sh +++ b/server/start.sh @@ -1 +1 @@ -DB_USER=postgres && DB_PASS=postgres && deno run --allow-env --allow-net --import-map=import_map.json main.ts +DB_USER=postgres DB_PASS=postgres deno run --allow-read --allow-env --allow-net --import-map=import_map.json main.ts