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
(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