diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..7d1e907 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "allowJs": false, + "lib": [ "ES2021", "DOM", "DOM.Iterable" ] + } +} diff --git a/server/Ingredient.ts b/server/Ingredient.ts deleted file mode 100644 index e2da97d..0000000 --- a/server/Ingredient.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { StoccaTreDbConn, WithoutId } from "./database.ts"; - -export type Ingredient = { - id: number, - name: string, - displayName: string, - displayNameDE: string, -}; - -export default class IngredientService { - private dbConnection: StoccaTreDbConn; - private mapById: Map = new Map(); - private allGotten = false; - - constructor(database: StoccaTreDbConn) { - this.dbConnection = database; - } - - async addIngredient(ingredient: WithoutId): Promise { - const result = await this.dbConnection.query( - `INSERT INTO ingredients (id, name, displayName, displayNameDE) VALUES (NULL, '${ingredient.name}', '${ingredient.displayName}', '${ingredient.displayNameDE}');` - ); - return result; - } - - async getAllIngredients(): Promise> { - if (!this.allGotten) { - const result = await this.dbConnection.query("SELECT * FROM ingredients"); - result.forEach((ingredient) => this.mapById.set(ingredient.id, ingredient)); - this.allGotten = true; - } - return this.mapById.values(); - } -} \ No newline at end of file diff --git a/server/StoccaTreRequest.ts b/server/StoccaTreRequest.ts new file mode 100644 index 0000000..afd728d --- /dev/null +++ b/server/StoccaTreRequest.ts @@ -0,0 +1,6 @@ +export type HttpMethod = "POST" | "GET" | "PUT" | "DELETE"; + +export default interface StoccaTreRequest { + method: HttpMethod, + route: string, +} diff --git a/server/StoccaTreRequestHandler.ts b/server/StoccaTreRequestHandler.ts new file mode 100644 index 0000000..2524e28 --- /dev/null +++ b/server/StoccaTreRequestHandler.ts @@ -0,0 +1,5 @@ +import StoccaTreRequest from "./StoccaTreRequest.ts"; + +export default interface StoccaTreRequestHandler { + handleRequest(request: StoccaTreRequest): Promise>, +} \ No newline at end of file diff --git a/server/config.ts b/server/config.ts new file mode 100644 index 0000000..a633c94 --- /dev/null +++ b/server/config.ts @@ -0,0 +1,6 @@ +type ServerConfig = { + username: string, + hostname: string, + password: string, + port: number, +} diff --git a/server/database.ts b/server/database.ts index a48786e..8d81c78 100644 --- a/server/database.ts +++ b/server/database.ts @@ -1,16 +1,40 @@ import { Client } from "https://deno.land/x/mysql/mod.ts"; -import dbconfig from "./config.json" assert { type: "json" }; +import config from "@/config.json" assert { type: "json" }; export type WithoutId = Omit; export interface StoccaTreDbConn { - query(query: string): Promise; + query(query: string): Promise>; } export default async function createNewDbConnection(): Promise { - return await new Client().connect({ - hostname: dbconfig.hostname, - username: dbconfig.username, + const mysqlClient = await new Client().connect({ + hostname: config.hostname, + username: config.username, db: "stocca_tre", - password: dbconfig.password, + password: config.password, }); + return { + query: async (query: string): Promise> => { + let errMessage: string; + try { + const result = await mysqlClient.query(query); + return { + just: result as T, + error: null, + }; + } catch (e: unknown) { + if (e && typeof (e as { message?: any }).message === "string") { + errMessage = (e as { message: string}).message; + } else { + errMessage = "An unknown error occurred in the database."; + } + } + return { + just: null, + error: { + message: errMessage + }, + }; + } + } } diff --git a/server/globalTypes.d.ts b/server/globalTypes.d.ts new file mode 100644 index 0000000..dd0aecc --- /dev/null +++ b/server/globalTypes.d.ts @@ -0,0 +1,20 @@ +type JSONValue = + | string + | number + | boolean + | JSONObject + | JSONArray; + +type JSONArray = Array; + +type Maybe = { + just: T, + error: null, +} | { + just: null, + error: { message: string }, +}; + +interface JSONObject { + [x: string]: JSONValue; +} diff --git a/server/import_map.json b/server/import_map.json new file mode 100644 index 0000000..793eacd --- /dev/null +++ b/server/import_map.json @@ -0,0 +1,5 @@ +{ + "imports": { + "/": "./" + } +} diff --git a/server/main.ts b/server/main.ts index 8d85772..06cdc4a 100644 --- a/server/main.ts +++ b/server/main.ts @@ -1,25 +1,59 @@ -import createNewDbConnection from "./database.ts"; -import IngredientService from "./Ingredient.ts"; +import createNewDbConnection, {StoccaTreDbConn} from "/database.ts"; +import * as resources from "/resources/main.ts"; +import config from "/config.json" assert { type: "json" }; +import StoccaTreRequest, {HttpMethod} from "/StoccaTreRequest.ts"; +import StoccaTreRequestHandler from "/StoccaTreRequestHandler.ts"; -const server = Deno.listen({ port: 8080 }); +const server = Deno.listen({ port: config.port ?? 8080 }); -console.log(`HTTP webserver running. Access it at: http://localhost:8080/`); +console.log(`Stocca Tre Server is running. Access it at: http://localhost:${ config.port }/`); + +type StoccaTreApiBody = { + data: JSONObject, + error?: string, +} + +function newStoccaTreApiBody(): StoccaTreApiBody { + return { + data: {}, + }; +} + +type StoccaTreServer = { + dbConnection: StoccaTreDbConn, + handleRequest(request: StoccaTreRequest): Promise>, +} + +async function processRequest(server: StoccaTreServer, requestEvent: Deno.RequestEvent) { + const route = requestEvent.request.destination; + const requestMethod = requestEvent.request.method; + console.log(route, requestMethod); + const body: StoccaTreApiBody = newStoccaTreApiBody(); + const maybeResult = await server.handleRequest({ + route, + method: requestMethod as HttpMethod, + }); + if (maybeResult.error) { + await requestEvent.respondWith(Response.json({ ...body, error: `Internal server error: ${maybeResult.error.message}` }, { status: 500 })); + } else { + body.data = maybeResult.just; + await requestEvent.respondWith(Response.json(body, { status: 200 })); + } +} const database = await createNewDbConnection(); -const ingredientService = new IngredientService(database); +const ingredientResource = new resources.IngredientResource(database); for await (const conn of server) { - serveHttp(conn); + await serveHttp(conn); } async function serveHttp(conn: Deno.Conn) { const httpConn = Deno.serveHttp(conn); - for await (const requestEvent of httpConn) { - const body = []; - for (const ingredient of (await ingredientService.getAllIngredients())) { - body.push(ingredient); - } - requestEvent.respondWith(Response.json(body, { status: 200 })); + await processRequest({ + dbConnection: database, + handleRequest: (request: StoccaTreRequest) => ingredientResource.handleRequest(request), + }, requestEvent); } -} \ No newline at end of file +} diff --git a/server/package.json b/server/package.json index d44b062..f1a7e3e 100644 --- a/server/package.json +++ b/server/package.json @@ -2,7 +2,7 @@ "name": "stocca-tre-server", "version": "0.0.1", "scripts": { - "start": "deno run --allow-net main.ts" + "start": "deno run --allow-net --import-map=importMap.json main.ts" }, "description": "Backend for StoccaTre", "author": "Daniel Ledda", diff --git a/server/resources/ingredient/IngredientCollection.ts b/server/resources/ingredient/IngredientCollection.ts new file mode 100644 index 0000000..ea3a211 --- /dev/null +++ b/server/resources/ingredient/IngredientCollection.ts @@ -0,0 +1,35 @@ +import {StoccaTreDbConn, WithoutId} from "../../database.ts"; +import {IngredientModel} from "./IngredientModel.ts"; + +export default class IngredientCollection { + private dbConnection: StoccaTreDbConn; + private mapById: Map = new Map(); + private allGotten = false; + + constructor(database: StoccaTreDbConn) { + this.dbConnection = database; + } + + async addIngredient(ingredient: WithoutId): Promise { + const result = await this.dbConnection.query( + `INSERT INTO ingredients (id, name, displayName, displayNameDE) VALUES (NULL, '${ingredient.name}', '${ingredient.displayName}', '${ingredient.displayNameDE}');` + ); + return result; + } + + async getAllIngredients(): Promise>> { + if (!this.allGotten) { + const result = await this.dbConnection.query("SELECT * FROM ingredients"); + if (result.just) { + result.just.forEach((ingredient) => this.mapById.set(ingredient.id, ingredient)); + } else { + return result; + } + this.allGotten = true; + } + return { + just: this.mapById.values(), + error: null, + }; + } +} diff --git a/server/resources/ingredient/IngredientModel.ts b/server/resources/ingredient/IngredientModel.ts new file mode 100644 index 0000000..27dd65d --- /dev/null +++ b/server/resources/ingredient/IngredientModel.ts @@ -0,0 +1,6 @@ +export type IngredientModel = { + id: number, + name: string, + displayName: string, + displayNameDE: string, +}; diff --git a/server/resources/ingredient/IngredientResource.ts b/server/resources/ingredient/IngredientResource.ts new file mode 100644 index 0000000..32f97f4 --- /dev/null +++ b/server/resources/ingredient/IngredientResource.ts @@ -0,0 +1,30 @@ +import {StoccaTreDbConn} from "/database.ts"; +import IngredientCollection from "IngredientCollection.ts"; +import StoccaTreRequest from "/StoccaTreRequest.ts"; + +export default class IngredientResource { + private dbConnection: StoccaTreDbConn; + private collection: IngredientCollection; + + constructor(dbConnection: StoccaTreDbConn) { + this.dbConnection = dbConnection; + this.collection = new IngredientCollection(dbConnection); + } + + async handleRequest(request: StoccaTreRequest): Promise> { + return await this.allIngredients(request); + } + + async allIngredients(request: StoccaTreRequest): Promise> { + const getAllIngredientResult = await this.collection.getAllIngredients(); + if (!getAllIngredientResult.error) { + return getAllIngredientResult.just; + } + return { + just: { + ingredients: Array.from(getAllIngredientResult.just), + }, + error: "", + }; + } +} \ No newline at end of file diff --git a/server/resources/ingredient/main.ts b/server/resources/ingredient/main.ts new file mode 100644 index 0000000..cb3f697 --- /dev/null +++ b/server/resources/ingredient/main.ts @@ -0,0 +1,3 @@ +export { type IngredientModel } from "./IngredientModel.ts"; +export { default as IngredientCollection } from "./IngredientCollection.ts"; +export { default as IngredientResource } from "./IngredientResource.ts"; diff --git a/server/resources/main.ts b/server/resources/main.ts new file mode 100644 index 0000000..6954655 --- /dev/null +++ b/server/resources/main.ts @@ -0,0 +1 @@ +export * from "./ingredient/main.ts"; diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..9c962e8 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "allowJs": false, + "lib": [ "ES2021" ] + } +} diff --git a/tsconfig.json b/tsconfig.json index d20b31f..377ad8e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ // "incremental": true, /* Enable incremental compilation */ "target": "ES6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ - // "lib": [], /* Specify library files to be included in the compilation. */ +// "lib": [""], /* Specify library files to be included in the compilation. */ "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */