From b21266e0e0f0d34dcfa6c7743faa35acecfc3a77 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 20 Feb 2024 14:10:07 +0100 Subject: [PATCH 01/31] Justfile targets adjusted for cleaning podman with podman-clean --- Justfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Justfile b/Justfile index 966fac9..eadfabd 100644 --- a/Justfile +++ b/Justfile @@ -28,4 +28,9 @@ clean: remove-podman-containers rm -rf frontend/node_modules rm -f ttime-server.tar.gz cd backend && make clean - @echo "Cleaned up!" \ No newline at end of file + @echo "Cleaned up!" + +# Cleans up everything related to podman, not just the project. Make sure you understand what this means. +[confirm] +podman-clean: + podman system reset --force \ No newline at end of file From 801d5cdb88cd94e0b5f51808f199b69f1651af94 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 20 Feb 2024 14:17:52 +0100 Subject: [PATCH 02/31] Better frontend docs --- frontend/README.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/frontend/README.md b/frontend/README.md index 5bb483c..58e7e3e 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -3,10 +3,30 @@ This is a React + TypeScript + Vite project. It is a single-page application (SPA) that communicates with the backend via a REST API. Most of this looks very complex, but it's mostly just a lot of boilerplate due to the ugodly amount of abstraction that comes with TypeScript and React, and just modern web development in general. +**Keep in mind** that the tooling for the frontend is configured to be extra strict (see the [eslint config](.eslintrc.cjs)). This is to make it hard to write bad code, by encouraging actual typing and not just using [any](https://www.typescriptlang.org/docs/handbook/basic-types.html#any) everywhere. This can be a bit of a struggle to work with at first, and I might have to backpedal on this if it becomes too much of a hassle. (Those familiar with Java will be more confused by the lack of types than the presence of them.) + ## Build Instructions -Our build tool, npm, is the only dependency required to build this project. It handles fetching the dependencies and placing them in the famously huge `node_modules` directory, also known as the heaviest object in the known universe. It also handles the build process, which is just a matter of running `npm run build`. +Our build tool, npm, is the only dependency required to build this project. It handles fetching the dependencies and placing them in the famously huge `node_modules` directory, also known as the heaviest object in the known universe. -The build target results in a `dist` directory, which contains the static assets that can be served by a web server. This is the directory that will be served by the backend. Think of this like compiling a program, the .exe/elf/binary is the `dist` directory. +### Development + +For a development session, in the `frontend` directory, run: + +```sh +npm install +npm run dev +``` For development, you use `npm run dev` to start a development server. This server will automatically rebuild the project when you make changes to the source code, and it will also automatically reload the page in your browser. The issue is that the development server will run without the backend, **so you will need to run the backend separately during development** if you want any meaningful data to be displayed. + +### Release + +The release build is usually automated by the CI/CD pipeline, but you can also build it manually. In the `frontend` directory, run: + +```sh +npm install +npm run build +``` + +The build target results in a `dist` directory, which contains the static assets that can be served by a web server. This is the directory that will be served by the backend. Think of this like compiling a program, the .exe/elf/binary is the `dist` directory. From ebe2a42f91e55446c2ef12448fb168a687e8dd59 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 20 Feb 2024 14:21:31 +0100 Subject: [PATCH 03/31] Windows note fixed --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index e5a686f..664a1e4 100644 --- a/BUILD.md +++ b/BUILD.md @@ -2,7 +2,7 @@ The build is fairly simple, and I intend to keep it that way. The project is split into two main parts: the backend and the frontend. -Making all of these parts work properly under linux/macOS is fairly simple, but Windows is untested territory. You can always contact [Billy G™®©](https://support.microsoft.com) for help. (If there is demand for Windows support, I will make it work) +Making all of these parts work properly under linux/macOS is fairly simple, but Windows will require [WSL](https://learn.microsoft.com/en-us/windows/wsl/). When working with these tools, keep in mind that make is sensitive to working directory. If you're in the wrong directory, make will not work as expected. Always make sure you're in the right directory when running make commands. From 4554ddab6d3944c62341224f17382a5c4e494faf Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 20 Feb 2024 15:26:28 +0100 Subject: [PATCH 04/31] Better button handler server-side --- backend/cmd/main.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 5ccb016..f06640a 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -12,15 +12,18 @@ import ( // The button state as represented in memory type ButtonState struct { - pressCount int + PressCount int `json:"pressCount"` } // This is what a handler with a receiver looks like // Keep in mind that concurrent state access is not (usually) safe // And will in pracice be guarded by a mutex func (b *ButtonState) pressHandler(w http.ResponseWriter, r *http.Request) { - b.pressCount++ - response, err := json.Marshal(b.pressCount) + if r.Method != "POST" { + b.PressCount++ + } + + response, err := json.Marshal(b) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -38,13 +41,13 @@ func handler(w http.ResponseWriter, r *http.Request) { func main() { database.DbConnect() - b := &ButtonState{pressCount: 0} + b := &ButtonState{PressCount: 0} // Mounting the handlers fs := http.FileServer(http.Dir("static")) http.Handle("/", fs) http.HandleFunc("/hello", handler) - http.HandleFunc("/button", b.pressHandler) + http.HandleFunc("/api/button", b.pressHandler) // Start the server on port 8080 println("Currently listening on http://localhost:8080") From d0d1a1dfaa7f8f8d69fc9b1f4df442f8c51e3e93 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 20 Feb 2024 15:27:15 +0100 Subject: [PATCH 05/31] Gluing together the frontend and backend --- frontend/src/App.tsx | 12 ++------ frontend/src/Components/CountButton.tsx | 39 +++++++++++++++++++++++++ frontend/vite.config.ts | 15 ++++++++++ 3 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 frontend/src/Components/CountButton.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9d045e1..20e5f1f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,9 @@ -import { useState } from "react"; import reactLogo from "./assets/react.svg"; import viteLogo from "/vite.svg"; import "./App.css"; +import { CountButton } from "./Components/CountButton"; function App(): JSX.Element { - const [count, setCount] = useState(0); - return ( <>
@@ -18,13 +16,7 @@ function App(): JSX.Element {

Vite + React

- +

Edit src/App.tsx and save to test HMR

diff --git a/frontend/src/Components/CountButton.tsx b/frontend/src/Components/CountButton.tsx new file mode 100644 index 0000000..cce55af --- /dev/null +++ b/frontend/src/Components/CountButton.tsx @@ -0,0 +1,39 @@ +import { useState, useEffect } from "react"; + +// Interface for the response from the server +// This should eventually reside in a dedicated file +interface CountResponse { + pressCount: number; +} + +// Some constants for the button +const BUTTON_ENDPOINT = "/api/button"; + +// A simple button that counts how many times it's been pressed +export function CountButton(): JSX.Element { + const [count, setCount] = useState(NaN); + + // useEffect with a [] dependency array runs only once + useEffect(() => { + async function getCount(): Promise { + const response = await fetch(BUTTON_ENDPOINT); + const data = (await response.json()) as CountResponse; + setCount(data.pressCount); + } + void getCount(); + }, []); + + // This is what runs on every button click + function press(): void { + async function pressPost(): Promise { + await fetch(BUTTON_ENDPOINT, { method: "POST" }); + const response = await fetch(BUTTON_ENDPOINT); + const data = (await response.json()) as CountResponse; + setCount(data.pressCount); + } + void pressPost(); + } + + // Return some JSX with the button and associated handler + return ; +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index d366e8c..f8ec812 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,4 +4,19 @@ import react from "@vitejs/plugin-react-swc"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + // build: { + // outDir: '../server/public' // Override default outDir('dist') + // }, + server: { + port: 3000, + open: true, + proxy: { + "/api": { + target: "http://localhost:8080/api", + changeOrigin: true, + secure: false, + rewrite: (path): string => path.replace(/^\/api/, ""), + }, + }, + }, }); From 540e8bcc79a45165a1831366f314ba1c1a406ecc Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 20 Feb 2024 15:42:45 +0100 Subject: [PATCH 06/31] Watch target for makefile in backend --- backend/Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/Makefile b/backend/Makefile index aeab862..0482c03 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -19,6 +19,9 @@ build: run: build ./bin/server +watch: build + watchexec -w . -r make run + # Clean target clean: $(GOCLEAN) From dec43d2dbac2a67192d7be14ef9c3fdfb2923dd3 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 20 Feb 2024 15:54:40 +0100 Subject: [PATCH 07/31] Typo --- backend/cmd/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index f06640a..5892d5a 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -19,7 +19,7 @@ type ButtonState struct { // Keep in mind that concurrent state access is not (usually) safe // And will in pracice be guarded by a mutex func (b *ButtonState) pressHandler(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { + if r.Method == "POST" { b.PressCount++ } From 3b51c699485437727627be107d8a2a8a37e8e06e Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 20 Feb 2024 15:55:15 +0100 Subject: [PATCH 08/31] Typo2 --- backend/cmd/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 5892d5a..699ebf5 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -17,7 +17,7 @@ type ButtonState struct { // This is what a handler with a receiver looks like // Keep in mind that concurrent state access is not (usually) safe -// And will in pracice be guarded by a mutex +// And will in practice be guarded by a mutex func (b *ButtonState) pressHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { b.PressCount++ From 1e96bbe3b6de9a831675fadfe820509237daa76f Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 20 Feb 2024 18:41:12 +0100 Subject: [PATCH 09/31] Fixed double fetch --- frontend/src/Components/CountButton.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/Components/CountButton.tsx b/frontend/src/Components/CountButton.tsx index cce55af..a6f1b30 100644 --- a/frontend/src/Components/CountButton.tsx +++ b/frontend/src/Components/CountButton.tsx @@ -26,8 +26,7 @@ export function CountButton(): JSX.Element { // This is what runs on every button click function press(): void { async function pressPost(): Promise { - await fetch(BUTTON_ENDPOINT, { method: "POST" }); - const response = await fetch(BUTTON_ENDPOINT); + const response = await fetch(BUTTON_ENDPOINT, { method: "POST" }); const data = (await response.json()) as CountResponse; setCount(data.pressCount); } From f4e6924bbb9f8a7aa7eb879dcef3d347b358675f Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 20 Feb 2024 18:56:10 +0100 Subject: [PATCH 10/31] Updating containerignore --- .containerignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.containerignore b/.containerignore index b5b7c6a..baa5ca5 100644 --- a/.containerignore +++ b/.containerignore @@ -1,4 +1,5 @@ **/target **/node_modules **/dist -**/.sqlite3 \ No newline at end of file +**/bin +**/*.sqlite3 From c3457011b17f8d8eaecb4273ace6b988f7dd934e Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 20 Feb 2024 19:44:26 +0100 Subject: [PATCH 11/31] Hardening the container --- container/Containerfile | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/container/Containerfile b/container/Containerfile index ba442de..23d177c 100644 --- a/container/Containerfile +++ b/container/Containerfile @@ -4,7 +4,7 @@ # make it available in the public directory. FROM docker.io/node:alpine as client WORKDIR /build -ADD frontend /build +ADD frontend ./ RUN npm install RUN npm run build @@ -13,31 +13,45 @@ FROM docker.io/golang:alpine as go RUN apk add gcompat RUN apk add gcc RUN apk add musl-dev -ADD backend /build +RUN apk add make +RUN apk add sqlite WORKDIR /build +ADD backend/go.mod backend/go.sum ./ # Get the dependencies RUN go mod download +# Add the source code +ADD backend . + +RUN make migrate + # RUN go build -o server RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o ./server ./cmd/ + +# Strip the binary for a smaller image RUN strip ./server # The final stage for building a minimal image FROM docker.io/alpine:latest as runner +RUN adduser -D nonroot +RUN addgroup nonroot nonroot WORKDIR /app # Copy the frontend SPA build into public -COPY --from=client /build/dist /app/static +COPY --from=client /build/dist static # Copy the server binary -COPY --from=go /build/server /app/server +COPY --from=go /build/server server -# Copy the migration scripts -COPY --from=go /build/migrations /app/migrations +# Copy the database +COPY --from=go /build/db.sqlite3 db.sqlite3 # Expose port 8080 EXPOSE 8080 +# Set the user to nonroot +USER nonroot:nonroot + # Run the server CMD ["./server"] \ No newline at end of file From 973ab1db1d3a46684dae081f0386f146c1298c09 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 20 Feb 2024 20:13:53 +0100 Subject: [PATCH 12/31] Config file TOML parsing with basic tests --- backend/go.mod | 2 + backend/go.sum | 2 + backend/internal/config/config.go | 55 +++++++++++++++++++++ backend/internal/config/config_test.go | 68 ++++++++++++++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 backend/internal/config/config.go create mode 100644 backend/internal/config/config_test.go diff --git a/backend/go.mod b/backend/go.mod index 34d4ebf..57c5c73 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -6,3 +6,5 @@ require ( github.com/jmoiron/sqlx v1.3.5 github.com/mattn/go-sqlite3 v1.14.22 ) + +require github.com/BurntSushi/toml v1.3.2 // indirect diff --git a/backend/go.sum b/backend/go.sum index ee85dd9..afab06f 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 0000000..aec7512 --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,55 @@ +package config + +import ( + "os" + + "github.com/BurntSushi/toml" +) + +// Config is the configuration for the application +// It defines how the TOML file should be structured +type Config struct { + // The port to listen on + Port int `toml:"port"` + // The path to the SQLite database + DbPath string `toml:"db_path"` + // The username to use for the database + DbUser string `toml:"db_user"` + // The password to use for the database + DbPass string `toml:"db_pass"` + // The name of the database + DbName string `toml:"db_name"` +} + +// WriteConfigToFile writes a Config to a file +func WriteConfigToFile(c *Config, filename string) error { + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + return toml.NewEncoder(f).Encode(c) +} + +// ReadConfigFromFile reads a Config from a file +func ReadConfigFromFile(filename string) (*Config, error) { + c := NewConfig() + _, err := toml.DecodeFile(filename, c) + if err != nil { + return nil, err + } + + return c, nil +} + +// NewConfig returns a new Config +func NewConfig() *Config { + return &Config{ + Port: 8080, + DbPath: "./db.sqlite3", + DbUser: "username", + DbPass: "password", + DbName: "ttime", + } +} diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go new file mode 100644 index 0000000..cc462ff --- /dev/null +++ b/backend/internal/config/config_test.go @@ -0,0 +1,68 @@ +package config + +import ( + "os" + "testing" +) + +func TestNewConfig(t *testing.T) { + c := NewConfig() + if c.Port != 8080 { + t.Errorf("Expected port to be 8080, got %d", c.Port) + } + if c.DbPath != "./db.sqlite3" { + t.Errorf("Expected db path to be ./db.sqlite3, got %s", c.DbPath) + } + if c.DbUser != "username" { + t.Errorf("Expected db user to be username, got %s", c.DbUser) + } + if c.DbPass != "password" { + t.Errorf("Expected db pass to be password, got %s", c.DbPass) + } + if c.DbName != "ttime" { + t.Errorf("Expected db name to be ttime, got %s", c.DbName) + } +} + +func TestWriteConfig(t *testing.T) { + c := NewConfig() + err := WriteConfigToFile(c, "test.toml") + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + // Remove the file after the test + _ = os.Remove("test.toml") +} + +func TestReadConfig(t *testing.T) { + c := NewConfig() + err := WriteConfigToFile(c, "test.toml") + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + c2, err := ReadConfigFromFile("test.toml") + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + if c.Port != c2.Port { + t.Errorf("Expected port to be %d, got %d", c.Port, c2.Port) + } + if c.DbPath != c2.DbPath { + t.Errorf("Expected db path to be %s, got %s", c.DbPath, c2.DbPath) + } + if c.DbUser != c2.DbUser { + t.Errorf("Expected db user to be %s, got %s", c.DbUser, c2.DbUser) + } + if c.DbPass != c2.DbPass { + t.Errorf("Expected db pass to be %s, got %s", c.DbPass, c2.DbPass) + } + if c.DbName != c2.DbName { + t.Errorf("Expected db name to be %s, got %s", c.DbName, c2.DbName) + } + + // Remove the file after the test + _ = os.Remove("test.toml") +} From b5fd6a1c4b6fbfc71fe193de2660a312a9a788f6 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 20 Feb 2024 20:24:53 +0100 Subject: [PATCH 13/31] Draft version of the SQL tables --- backend/migrations/0010.sql | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/backend/migrations/0010.sql b/backend/migrations/0010.sql index 11be51c..df314b1 100644 --- a/backend/migrations/0010.sql +++ b/backend/migrations/0010.sql @@ -5,3 +5,46 @@ CREATE TABLE IF NOT EXISTS users ( username VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL ); + +CREATE TABLE IF NOT EXISTS projects ( + id INTEGER PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) +); + +CREATE TABLE IF NOT EXISTS time_reports ( + id INTEGER PRIMARY KEY, + project_id INTEGER NOT NULL, + start DATETIME NOT NULL, + end DATETIME NOT NULL, + FOREIGN KEY (project_id) REFERENCES projects (id) +); + +CREATE TRIGGER IF NOT EXISTS time_reports_start_before_end + BEFORE INSERT ON time_reports + FOR EACH ROW + BEGIN + SELECT + CASE + WHEN NEW.start >= NEW.end THEN + RAISE (ABORT, 'start must be before end') + END; + END; + +CREATE TABLE IF NOT EXISTS report_collection ( + id INTEGER PRIMARY KEY, + project_id INTEGER NOT NULL, + date DATE NOT NULL, + signed_by INTEGER, -- NULL if not signed + FOREIGN KEY (signed_by) REFERENCES users (id) +); + +CREATE TABLE IF NOT EXISTS user_roles ( + user_id INTEGER NOT NULL, + project_id INTEGER NOT NULL, + role STRING NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) + FOREIGN KEY (project_id) REFERENCES projects (id) +); \ No newline at end of file From f9eb67da112c37d1c1e5a13c04b3ec8bdb9dbf99 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Mon, 26 Feb 2024 00:06:04 +0100 Subject: [PATCH 14/31] Direct dependency specification in go.mod --- backend/go.mod | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index 57c5c73..8c06f45 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -5,6 +5,5 @@ go 1.21.1 require ( github.com/jmoiron/sqlx v1.3.5 github.com/mattn/go-sqlite3 v1.14.22 + github.com/BurntSushi/toml v1.3.2 ) - -require github.com/BurntSushi/toml v1.3.2 // indirect From 3ba446be5b535beb308879beb417583e62624fb0 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Mon, 26 Feb 2024 00:12:13 +0100 Subject: [PATCH 15/31] Splitting migration scripts --- backend/migrations/0010.sql | 50 ------------------- backend/migrations/0010_users.sql | 5 ++ backend/migrations/0020_projects.sql | 7 +++ backend/migrations/0030_time_reports.sql | 18 +++++++ .../0040_time_report_collections.sql | 7 +++ backend/migrations/0050_user_roles.sql | 7 +++ 6 files changed, 44 insertions(+), 50 deletions(-) delete mode 100644 backend/migrations/0010.sql create mode 100644 backend/migrations/0010_users.sql create mode 100644 backend/migrations/0020_projects.sql create mode 100644 backend/migrations/0030_time_reports.sql create mode 100644 backend/migrations/0040_time_report_collections.sql create mode 100644 backend/migrations/0050_user_roles.sql diff --git a/backend/migrations/0010.sql b/backend/migrations/0010.sql deleted file mode 100644 index df314b1..0000000 --- a/backend/migrations/0010.sql +++ /dev/null @@ -1,50 +0,0 @@ -PRAGMA foreign_keys = ON; - -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY, - username VARCHAR(255) NOT NULL, - password VARCHAR(255) NOT NULL -); - -CREATE TABLE IF NOT EXISTS projects ( - id INTEGER PRIMARY KEY, - name VARCHAR(255) NOT NULL, - description TEXT NOT NULL, - user_id INTEGER NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id) -); - -CREATE TABLE IF NOT EXISTS time_reports ( - id INTEGER PRIMARY KEY, - project_id INTEGER NOT NULL, - start DATETIME NOT NULL, - end DATETIME NOT NULL, - FOREIGN KEY (project_id) REFERENCES projects (id) -); - -CREATE TRIGGER IF NOT EXISTS time_reports_start_before_end - BEFORE INSERT ON time_reports - FOR EACH ROW - BEGIN - SELECT - CASE - WHEN NEW.start >= NEW.end THEN - RAISE (ABORT, 'start must be before end') - END; - END; - -CREATE TABLE IF NOT EXISTS report_collection ( - id INTEGER PRIMARY KEY, - project_id INTEGER NOT NULL, - date DATE NOT NULL, - signed_by INTEGER, -- NULL if not signed - FOREIGN KEY (signed_by) REFERENCES users (id) -); - -CREATE TABLE IF NOT EXISTS user_roles ( - user_id INTEGER NOT NULL, - project_id INTEGER NOT NULL, - role STRING NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id) - FOREIGN KEY (project_id) REFERENCES projects (id) -); \ No newline at end of file diff --git a/backend/migrations/0010_users.sql b/backend/migrations/0010_users.sql new file mode 100644 index 0000000..d36847e --- /dev/null +++ b/backend/migrations/0010_users.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + username VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL +); \ No newline at end of file diff --git a/backend/migrations/0020_projects.sql b/backend/migrations/0020_projects.sql new file mode 100644 index 0000000..293aea5 --- /dev/null +++ b/backend/migrations/0020_projects.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS projects ( + id INTEGER PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) +); \ No newline at end of file diff --git a/backend/migrations/0030_time_reports.sql b/backend/migrations/0030_time_reports.sql new file mode 100644 index 0000000..b860a35 --- /dev/null +++ b/backend/migrations/0030_time_reports.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS time_reports ( + id INTEGER PRIMARY KEY, + project_id INTEGER NOT NULL, + start DATETIME NOT NULL, + end DATETIME NOT NULL, + FOREIGN KEY (project_id) REFERENCES projects (id) +); + +CREATE TRIGGER IF NOT EXISTS time_reports_start_before_end + BEFORE INSERT ON time_reports + FOR EACH ROW + BEGIN + SELECT + CASE + WHEN NEW.start >= NEW.end THEN + RAISE (ABORT, 'start must be before end') + END; + END; \ No newline at end of file diff --git a/backend/migrations/0040_time_report_collections.sql b/backend/migrations/0040_time_report_collections.sql new file mode 100644 index 0000000..054593b --- /dev/null +++ b/backend/migrations/0040_time_report_collections.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS user_roles ( + user_id INTEGER NOT NULL, + project_id INTEGER NOT NULL, + role STRING NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) + FOREIGN KEY (project_id) REFERENCES projects (id) +); \ No newline at end of file diff --git a/backend/migrations/0050_user_roles.sql b/backend/migrations/0050_user_roles.sql new file mode 100644 index 0000000..054593b --- /dev/null +++ b/backend/migrations/0050_user_roles.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS user_roles ( + user_id INTEGER NOT NULL, + project_id INTEGER NOT NULL, + role STRING NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) + FOREIGN KEY (project_id) REFERENCES projects (id) +); \ No newline at end of file From 033402ffaf2d99652edd253ccf80fbcf2d813afb Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Mon, 26 Feb 2024 00:13:39 +0100 Subject: [PATCH 16/31] Mistake in migration scripts --- backend/migrations/0040_time_report_collections.sql | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/migrations/0040_time_report_collections.sql b/backend/migrations/0040_time_report_collections.sql index 054593b..7dbe6bd 100644 --- a/backend/migrations/0040_time_report_collections.sql +++ b/backend/migrations/0040_time_report_collections.sql @@ -1,7 +1,7 @@ -CREATE TABLE IF NOT EXISTS user_roles ( - user_id INTEGER NOT NULL, +CREATE TABLE IF NOT EXISTS report_collection ( + id INTEGER PRIMARY KEY, project_id INTEGER NOT NULL, - role STRING NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id) - FOREIGN KEY (project_id) REFERENCES projects (id) + date DATE NOT NULL, + signed_by INTEGER, -- NULL if not signed + FOREIGN KEY (signed_by) REFERENCES users (id) ); \ No newline at end of file From 296ed987d8067b9f1914d444c46b43af6529204f Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 27 Feb 2024 04:56:12 +0100 Subject: [PATCH 17/31] Make tests verbose and disable testing cache --- backend/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/Makefile b/backend/Makefile index 0482c03..c76e28b 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -30,7 +30,7 @@ clean: # Test target test: - $(GOTEST) ./... + $(GOTEST) ./... -count=1 -v # Get dependencies target deps: @@ -50,4 +50,4 @@ migrate: done # Default target -default: build \ No newline at end of file +default: build From 5be29d86af6db323c3aea129e68c1cc943982112 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 27 Feb 2024 04:59:24 +0100 Subject: [PATCH 18/31] Makefile changes so that test depends on db.sqlite3 --- backend/Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/Makefile b/backend/Makefile index c76e28b..c417e73 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -29,7 +29,7 @@ clean: rm -f db.sqlite3 # Test target -test: +test: db.sqlite3 $(GOTEST) ./... -count=1 -v # Get dependencies target @@ -49,5 +49,9 @@ migrate: sqlite3 $(DB_FILE) < $$file; \ done +# Target added primarily for CI/CD to ensure that the database is created before running tests +db.sqlite3: + make migrate + # Default target default: build From 06076f93b7500dcd000d8660f90f9bd9ce3729e0 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 27 Feb 2024 05:00:04 +0100 Subject: [PATCH 19/31] Database interactions demo --- backend/internal/database/db.go | 36 ++++++++++++++++++++++++---- backend/internal/database/db_test.go | 16 +++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index d99efde..48ea95c 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -7,26 +7,54 @@ import ( _ "github.com/mattn/go-sqlite3" ) -func DbConnect() *sqlx.DB { +// This struct is a wrapper type that holds the database connection +// Internally DB holds a connection pool, so it's safe for concurrent use +type Db struct { + *sqlx.DB +} + +const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" + +// DbConnect connects to the database +func DbConnect() *Db { // Check for the environment variable dbpath := os.Getenv("SQLITE_DB_PATH") // Default to something reasonable if dbpath == "" { - dbpath = "./db.sqlite3" + // This should obviously not be like this + dbpath = "../../db.sqlite3" // This is disaster waiting to happen + // WARNING + + // If the file doesn't exist, panic + if _, err := os.Stat(dbpath); os.IsNotExist(err) { + panic("Database file does not exist: " + dbpath) + } } // Open the database - // db, err := sqlx.Connect("sqlite3", ":memory:") db, err := sqlx.Connect("sqlite3", dbpath) if err != nil { panic(err) } + // Ping forces the connection to be established err = db.Ping() if err != nil { panic(err) } - return db + return &Db{db} +} + +// AddUser adds a user to the database +func (d *Db) AddUser(username string, password string) error { + _, err := d.Exec(userInsert, username, password) + return err +} + +// Removes a user from the database +func (d *Db) RemoveUser(username string) error { + _, err := d.Exec("DELETE FROM users WHERE username = ?", username) + return err } diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index d7b26c9..6027ad1 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -8,3 +8,19 @@ func TestDbConnect(t *testing.T) { db := DbConnect() _ = db } + +func TestDbAddUser(t *testing.T) { + db := DbConnect() + err := db.AddUser("test", "password") + if err != nil { + t.Error("AddUser failed:", err) + } +} + +func TestDbRemoveUser(t *testing.T) { + db := DbConnect() + err := db.RemoveUser("test") + if err != nil { + t.Error("RemoveUser failed:", err) + } +} From 60e7b73f660192246285cf0a21f3b5aa8513591b Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 27 Feb 2024 05:50:28 +0100 Subject: [PATCH 20/31] Unique username constraint on users table --- backend/migrations/0010_users.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/migrations/0010_users.sql b/backend/migrations/0010_users.sql index d36847e..75c286c 100644 --- a/backend/migrations/0010_users.sql +++ b/backend/migrations/0010_users.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, - username VARCHAR(255) NOT NULL, + username VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL ); \ No newline at end of file From ce1ce89b000f2d991e038f5df0e4696b1936d04c Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 27 Feb 2024 05:51:16 +0100 Subject: [PATCH 21/31] Some more example database interface code --- backend/internal/database/db.go | 15 +++++++++++++++ backend/internal/database/db_test.go | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 48ea95c..335a4ea 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -58,3 +58,18 @@ func (d *Db) RemoveUser(username string) error { _, err := d.Exec("DELETE FROM users WHERE username = ?", username) return err } + +func (d *Db) GetUserId(username string) (int, error) { + var id int + err := d.Get(&id, "SELECT id FROM users WHERE username = ?", username) + return id, err +} + +func (d *Db) AddProject(name string, description string, username string) error { + userId, err := d.GetUserId(username) + if err != nil { + return err + } + _, err = d.Exec("INSERT INTO projects (name, description, user_id) VALUES (?, ?, ?)", name, description, userId) + return err +} diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 6027ad1..bad7dac 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -4,6 +4,9 @@ import ( "testing" ) +// Tests are not guaranteed to be sequential +// Writing tests like this will bite you, eventually + func TestDbConnect(t *testing.T) { db := DbConnect() _ = db @@ -17,6 +20,20 @@ func TestDbAddUser(t *testing.T) { } } +func TestDbGetUserId(t *testing.T) { + db := DbConnect() + + var id int + + id, err := db.GetUserId("test") + if err != nil { + t.Error("GetUserId failed:", err) + } + if id != 1 { + t.Error("GetUserId failed: expected 1, got", id) + } +} + func TestDbRemoveUser(t *testing.T) { db := DbConnect() err := db.RemoveUser("test") From 6e48c0a0888448f901fd3261e04a52876b7e4541 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 27 Feb 2024 06:09:09 +0100 Subject: [PATCH 22/31] Database migration readme --- backend/migrations/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 backend/migrations/README.md diff --git a/backend/migrations/README.md b/backend/migrations/README.md new file mode 100644 index 0000000..18b5e34 --- /dev/null +++ b/backend/migrations/README.md @@ -0,0 +1,14 @@ +# Database migrations + +This directory contains all the database migrations for the backend. + +[!WARNING] +Keep in mind that these migrations are **not yet stable**. + +## Running migrations + +In the root of the backend directory, run: + +```bash +make migrate +``` From 6c82aa2047c58ae6daa2b507a7497639e50da32b Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 27 Feb 2024 07:43:20 +0100 Subject: [PATCH 23/31] Backup target for makefile --- backend/Makefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/Makefile b/backend/Makefile index c417e73..6318212 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -53,5 +53,10 @@ migrate: db.sqlite3: make migrate +backup: + sqlite3 $(DB_FILE) .dump | gzip -9 > BACKUP_$(DB_FILE)_$(shell date +"%Y-%m-%d_%H:%M:%S").sql.gz + # Restore with: + # gzip -cd BACKUP_$(DB_FILE)_*.sql.gz | sqlite3 $(DB_FILE) + # Default target default: build From 6f51151d64371c374c9deb54198c0cb881b5c7c9 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 27 Feb 2024 07:45:38 +0100 Subject: [PATCH 24/31] Save backups in a backup directory --- .gitignore | 3 +-- backend/Makefile | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 063a7e7..1d09217 100644 --- a/.gitignore +++ b/.gitignore @@ -48,5 +48,4 @@ dist/ *.7z *.bak - - +backend/backups diff --git a/backend/Makefile b/backend/Makefile index 6318212..db8094c 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -54,9 +54,10 @@ db.sqlite3: make migrate backup: - sqlite3 $(DB_FILE) .dump | gzip -9 > BACKUP_$(DB_FILE)_$(shell date +"%Y-%m-%d_%H:%M:%S").sql.gz + mkdir -p backups + sqlite3 $(DB_FILE) .dump | gzip -9 > ./backups/BACKUP_$(DB_FILE)_$(shell date +"%Y-%m-%d_%H:%M:%S").sql.gz # Restore with: - # gzip -cd BACKUP_$(DB_FILE)_*.sql.gz | sqlite3 $(DB_FILE) + # gzip -cd BACKUP_FILE.sql.gz | sqlite3 $(DB_FILE) # Default target default: build From 229ebc3a088a2a8c41879f5b7cc08cd663ec883f Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 27 Feb 2024 07:59:42 +0100 Subject: [PATCH 25/31] AddProject database interface with tests --- backend/internal/database/db.go | 8 +++----- backend/internal/database/db_test.go | 8 ++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 335a4ea..e0006d1 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -14,6 +14,7 @@ type Db struct { } const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" +const projectInsert = "INSERT INTO projects (name, description, user_id) SELECT ?, ?, id FROM users WHERE username = ?" // DbConnect connects to the database func DbConnect() *Db { @@ -65,11 +66,8 @@ func (d *Db) GetUserId(username string) (int, error) { return id, err } +// Creates a new project in the database, associated with a user func (d *Db) AddProject(name string, description string, username string) error { - userId, err := d.GetUserId(username) - if err != nil { - return err - } - _, err = d.Exec("INSERT INTO projects (name, description, user_id) VALUES (?, ?, ?)", name, description, userId) + _, err := d.Exec(projectInsert, name, description, username) return err } diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index bad7dac..ab0355a 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -34,6 +34,14 @@ func TestDbGetUserId(t *testing.T) { } } +func TestDbAddProject(t *testing.T) { + db := DbConnect() + err := db.AddProject("test", "description", "test") + if err != nil { + t.Error("AddProject failed:", err) + } +} + func TestDbRemoveUser(t *testing.T) { db := DbConnect() err := db.RemoveUser("test") From 9d6cddb7245e6351b41332c63943fa0470fca4a1 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 27 Feb 2024 08:05:07 +0100 Subject: [PATCH 26/31] Various breaking changes to the database schema --- backend/migrations/0010_users.sql | 6 +++++- backend/migrations/0020_projects.sql | 8 ++++++-- backend/migrations/0030_time_reports.sql | 3 ++- backend/migrations/0040_time_report_collections.sql | 2 ++ backend/migrations/0050_user_roles.sql | 2 +- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/backend/migrations/0010_users.sql b/backend/migrations/0010_users.sql index 75c286c..7cb23fd 100644 --- a/backend/migrations/0010_users.sql +++ b/backend/migrations/0010_users.sql @@ -1,5 +1,9 @@ CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, + userId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE, username VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL -); \ No newline at end of file +); + +CREATE INDEX IF NOT EXISTS users_username_index ON users (username); +CREATE INDEX IF NOT EXISTS users_userId_index ON users (userId); \ No newline at end of file diff --git a/backend/migrations/0020_projects.sql b/backend/migrations/0020_projects.sql index 293aea5..8592e75 100644 --- a/backend/migrations/0020_projects.sql +++ b/backend/migrations/0020_projects.sql @@ -1,7 +1,11 @@ CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY, - name VARCHAR(255) NOT NULL, + projectId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL UNIQUE, description TEXT NOT NULL, user_id INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) -); \ No newline at end of file +); + +CREATE INDEX IF NOT EXISTS projects_projectId_index ON projects (projectId); +CREATE INDEX IF NOT EXISTS projects_user_id_index ON projects (user_id); \ No newline at end of file diff --git a/backend/migrations/0030_time_reports.sql b/backend/migrations/0030_time_reports.sql index b860a35..e8f3ec1 100644 --- a/backend/migrations/0030_time_reports.sql +++ b/backend/migrations/0030_time_reports.sql @@ -1,9 +1,10 @@ CREATE TABLE IF NOT EXISTS time_reports ( id INTEGER PRIMARY KEY, + reportId TEXT DEFAULT (HEX(RANDOMBLOB(6))) NOT NULL UNIQUE, project_id INTEGER NOT NULL, start DATETIME NOT NULL, end DATETIME NOT NULL, - FOREIGN KEY (project_id) REFERENCES projects (id) + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE ); CREATE TRIGGER IF NOT EXISTS time_reports_start_before_end diff --git a/backend/migrations/0040_time_report_collections.sql b/backend/migrations/0040_time_report_collections.sql index 7dbe6bd..be406ff 100644 --- a/backend/migrations/0040_time_report_collections.sql +++ b/backend/migrations/0040_time_report_collections.sql @@ -1,7 +1,9 @@ CREATE TABLE IF NOT EXISTS report_collection ( id INTEGER PRIMARY KEY, + owner_id INTEGER NOT NULL, project_id INTEGER NOT NULL, date DATE NOT NULL, signed_by INTEGER, -- NULL if not signed + FOREIGN KEY (owner_id) REFERENCES users (id) FOREIGN KEY (signed_by) REFERENCES users (id) ); \ No newline at end of file diff --git a/backend/migrations/0050_user_roles.sql b/backend/migrations/0050_user_roles.sql index 054593b..32e03dc 100644 --- a/backend/migrations/0050_user_roles.sql +++ b/backend/migrations/0050_user_roles.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS user_roles ( user_id INTEGER NOT NULL, project_id INTEGER NOT NULL, - role STRING NOT NULL, + role STRING NOT NULL, -- 'admin' or 'member' FOREIGN KEY (user_id) REFERENCES users (id) FOREIGN KEY (project_id) REFERENCES projects (id) ); \ No newline at end of file From 06632c16da7bae37ef4bd79ce5e92ecd4fddee52 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Tue, 27 Feb 2024 23:11:27 +0100 Subject: [PATCH 27/31] Better interface for writeconfig --- backend/internal/config/config.go | 2 +- backend/internal/config/config_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index aec7512..5bebc34 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -22,7 +22,7 @@ type Config struct { } // WriteConfigToFile writes a Config to a file -func WriteConfigToFile(c *Config, filename string) error { +func (c *Config) WriteConfigToFile(filename string) error { f, err := os.Create(filename) if err != nil { return err diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go index cc462ff..cb02a31 100644 --- a/backend/internal/config/config_test.go +++ b/backend/internal/config/config_test.go @@ -26,7 +26,7 @@ func TestNewConfig(t *testing.T) { func TestWriteConfig(t *testing.T) { c := NewConfig() - err := WriteConfigToFile(c, "test.toml") + err := c.WriteConfigToFile("test.toml") if err != nil { t.Errorf("Expected no error, got %s", err) } @@ -37,7 +37,7 @@ func TestWriteConfig(t *testing.T) { func TestReadConfig(t *testing.T) { c := NewConfig() - err := WriteConfigToFile(c, "test.toml") + err := c.WriteConfigToFile("test.toml") if err != nil { t.Errorf("Expected no error, got %s", err) } From e1cd596c13d33a0ac99c079e9678d9653724e82b Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Wed, 28 Feb 2024 03:21:13 +0100 Subject: [PATCH 28/31] Config parsing and database api changes --- backend/cmd/main.go | 13 +++++-- backend/internal/database/db.go | 53 +++++++++++++++++++--------- backend/internal/database/db_test.go | 43 ++++++++++++++++------ 3 files changed, 81 insertions(+), 28 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 699ebf5..bb71e31 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "ttime/internal/config" "ttime/internal/database" _ "github.com/mattn/go-sqlite3" @@ -40,7 +41,15 @@ func handler(w http.ResponseWriter, r *http.Request) { } func main() { - database.DbConnect() + conf, err := config.ReadConfigFromFile("config.toml") + if err != nil { + conf = config.NewConfig() + conf.WriteConfigToFile("config.toml") + } + + println(conf) + + database.DbConnect("db.sqlite3") b := &ButtonState{PressCount: 0} // Mounting the handlers @@ -54,7 +63,7 @@ func main() { println("Visit http://localhost:8080/hello to see the hello handler in action") println("Visit http://localhost:8080/button to see the button handler in action") println("Press Ctrl+C to stop the server") - err := http.ListenAndServe(":8080", nil) + err = http.ListenAndServe(":8080", nil) if err != nil { panic(err) } diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index e0006d1..0334cc4 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -1,7 +1,9 @@ package database import ( + "log" "os" + "path/filepath" "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" @@ -17,22 +19,7 @@ const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" const projectInsert = "INSERT INTO projects (name, description, user_id) SELECT ?, ?, id FROM users WHERE username = ?" // DbConnect connects to the database -func DbConnect() *Db { - // Check for the environment variable - dbpath := os.Getenv("SQLITE_DB_PATH") - - // Default to something reasonable - if dbpath == "" { - // This should obviously not be like this - dbpath = "../../db.sqlite3" // This is disaster waiting to happen - // WARNING - - // If the file doesn't exist, panic - if _, err := os.Stat(dbpath); os.IsNotExist(err) { - panic("Database file does not exist: " + dbpath) - } - } - +func DbConnect(dbpath string) *Db { // Open the database db, err := sqlx.Connect("sqlite3", dbpath) if err != nil { @@ -71,3 +58,37 @@ func (d *Db) AddProject(name string, description string, username string) error _, err := d.Exec(projectInsert, name, description, username) return err } + +// Reads a directory of migration files and applies them to the database. +// This will eventually be used on an embedded directory +func (d *Db) Migrate(dirname string) error { + files, err := os.ReadDir(dirname) + if err != nil { + return err + } + + tr := d.MustBegin() + + // Iterate over each SQL file and execute it + for _, file := range files { + if file.IsDir() || filepath.Ext(file.Name()) != ".sql" { + continue + } + + sqlFile := filepath.Join(dirname, file.Name()) + sqlBytes, err := os.ReadFile(sqlFile) + if err != nil { + return err + } + + sqlQuery := string(sqlBytes) + _, err = tr.Exec(sqlQuery) + if err != nil { + return err + } + log.Println("Executed SQL file:", file.Name()) + } + + tr.Commit() + return nil +} diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index ab0355a..62e47db 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -5,27 +5,42 @@ import ( ) // Tests are not guaranteed to be sequential -// Writing tests like this will bite you, eventually + +func setupState() (*Db, error) { + db := DbConnect(":memory:") + err := db.Migrate("../../migrations") + if err != nil { + return nil, err + } + return db, nil +} func TestDbConnect(t *testing.T) { - db := DbConnect() + db := DbConnect(":memory:") _ = db } func TestDbAddUser(t *testing.T) { - db := DbConnect() - err := db.AddUser("test", "password") + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + err = db.AddUser("test", "password") if err != nil { t.Error("AddUser failed:", err) } } func TestDbGetUserId(t *testing.T) { - db := DbConnect() + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + db.AddUser("test", "password") var id int - id, err := db.GetUserId("test") + id, err = db.GetUserId("test") if err != nil { t.Error("GetUserId failed:", err) } @@ -35,16 +50,24 @@ func TestDbGetUserId(t *testing.T) { } func TestDbAddProject(t *testing.T) { - db := DbConnect() - err := db.AddProject("test", "description", "test") + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddProject("test", "description", "test") if err != nil { t.Error("AddProject failed:", err) } } func TestDbRemoveUser(t *testing.T) { - db := DbConnect() - err := db.RemoveUser("test") + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.RemoveUser("test") if err != nil { t.Error("RemoveUser failed:", err) } From 77e3324f79297842207b71be40eaad32c1f90de6 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Wed, 28 Feb 2024 03:21:33 +0100 Subject: [PATCH 29/31] Gitignore the config file --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1d09217..9ad11ec 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ dist/ *.bak backend/backups + +config.toml From 34c071282511f82d1c053647c753f32a9d1704f6 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Wed, 28 Feb 2024 03:30:05 +0100 Subject: [PATCH 30/31] Moving the migrations directory into database for embedding --- backend/internal/database/db.go | 10 ++++++++-- .../{ => internal/database}/migrations/0010_users.sql | 0 .../database}/migrations/0020_projects.sql | 0 .../database}/migrations/0030_time_reports.sql | 0 .../migrations/0040_time_report_collections.sql | 0 .../database}/migrations/0050_user_roles.sql | 0 backend/{ => internal/database}/migrations/README.md | 0 7 files changed, 8 insertions(+), 2 deletions(-) rename backend/{ => internal/database}/migrations/0010_users.sql (100%) rename backend/{ => internal/database}/migrations/0020_projects.sql (100%) rename backend/{ => internal/database}/migrations/0030_time_reports.sql (100%) rename backend/{ => internal/database}/migrations/0040_time_report_collections.sql (100%) rename backend/{ => internal/database}/migrations/0050_user_roles.sql (100%) rename backend/{ => internal/database}/migrations/README.md (100%) diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 0334cc4..c14ba97 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -1,6 +1,7 @@ package database import ( + "embed" "log" "os" "path/filepath" @@ -15,6 +16,9 @@ type Db struct { *sqlx.DB } +//go:embed migrations +var scripts embed.FS + const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" const projectInsert = "INSERT INTO projects (name, description, user_id) SELECT ?, ?, id FROM users WHERE username = ?" @@ -62,7 +66,8 @@ func (d *Db) AddProject(name string, description string, username string) error // Reads a directory of migration files and applies them to the database. // This will eventually be used on an embedded directory func (d *Db) Migrate(dirname string) error { - files, err := os.ReadDir(dirname) + // Read the embedded scripts directory + files, err := scripts.ReadDir("migrations") if err != nil { return err } @@ -75,7 +80,8 @@ func (d *Db) Migrate(dirname string) error { continue } - sqlFile := filepath.Join(dirname, file.Name()) + // This is perhaps not the most elegant way to do this + sqlFile := filepath.Join("migrations", file.Name()) sqlBytes, err := os.ReadFile(sqlFile) if err != nil { return err diff --git a/backend/migrations/0010_users.sql b/backend/internal/database/migrations/0010_users.sql similarity index 100% rename from backend/migrations/0010_users.sql rename to backend/internal/database/migrations/0010_users.sql diff --git a/backend/migrations/0020_projects.sql b/backend/internal/database/migrations/0020_projects.sql similarity index 100% rename from backend/migrations/0020_projects.sql rename to backend/internal/database/migrations/0020_projects.sql diff --git a/backend/migrations/0030_time_reports.sql b/backend/internal/database/migrations/0030_time_reports.sql similarity index 100% rename from backend/migrations/0030_time_reports.sql rename to backend/internal/database/migrations/0030_time_reports.sql diff --git a/backend/migrations/0040_time_report_collections.sql b/backend/internal/database/migrations/0040_time_report_collections.sql similarity index 100% rename from backend/migrations/0040_time_report_collections.sql rename to backend/internal/database/migrations/0040_time_report_collections.sql diff --git a/backend/migrations/0050_user_roles.sql b/backend/internal/database/migrations/0050_user_roles.sql similarity index 100% rename from backend/migrations/0050_user_roles.sql rename to backend/internal/database/migrations/0050_user_roles.sql diff --git a/backend/migrations/README.md b/backend/internal/database/migrations/README.md similarity index 100% rename from backend/migrations/README.md rename to backend/internal/database/migrations/README.md From 2349fad4f49ef8ee934855d7f4f2fc313a22c297 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Wed, 28 Feb 2024 08:39:42 +0100 Subject: [PATCH 31/31] Make use of the config --- backend/cmd/main.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index bb71e31..ea8d968 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -47,9 +47,11 @@ func main() { conf.WriteConfigToFile("config.toml") } - println(conf) + // Pretty print the current config + str, _ := json.MarshalIndent(conf, "", " ") + fmt.Println(string(str)) - database.DbConnect("db.sqlite3") + database.DbConnect(conf.DbPath) b := &ButtonState{PressCount: 0} // Mounting the handlers @@ -58,12 +60,14 @@ func main() { http.HandleFunc("/hello", handler) http.HandleFunc("/api/button", b.pressHandler) - // Start the server on port 8080 - println("Currently listening on http://localhost:8080") - println("Visit http://localhost:8080/hello to see the hello handler in action") - println("Visit http://localhost:8080/button to see the button handler in action") + // Construct a server URL + server_url := fmt.Sprintf(":%d", conf.Port) + + println("Server running on port", conf.Port) + println("Visit http://localhost" + server_url) println("Press Ctrl+C to stop the server") - err = http.ListenAndServe(":8080", nil) + + err = http.ListenAndServe(server_url, nil) if err != nil { panic(err) }