From 84da650a8f0d2b90e43af2d687ee3b6e8671cecb Mon Sep 17 00:00:00 2001 From: EmilVisuri <156796516+EmilVisuri@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:20:18 +0200 Subject: [PATCH] Emil/db migrations (#58) * Add users table migration * Add users table migration and update schema * Add users table migration * updated schema, users table and .env.test * added public.users table * Add users table migration * Add users table migration * more fixing * prettier, husky and editorconfig * husky * update actions * Move deps * fixed workflow * regenerate schema * fix generated schema * move to dev dep --------- Co-authored-by: Visuri Emil A Co-authored-by: DoebelnAtz --- .editorconfig | 39 ++ .github/workflows/github-actions.yml | 4 + .gitignore | 1 + .husky/pre-commit | 4 + .prettierignore | 5 + .prettierrc | 5 + README.md | 1 + data/migrations/00002_add_users_table.sql | 5 + data/schema.sql | 6 + gradesa/.eslintrc.js | 20 + gradesa/eslint.config.mjs | 14 - gradesa/package-lock.json | 55 ++ gradesa/package.json | 6 +- gradesa/src/app/api/hello/route.js | 12 +- gradesa/src/app/auth/layout.js | 16 +- gradesa/src/app/auth/login/page.js | 30 +- gradesa/src/app/page.js | 10 +- gradesa/src/backend/config.js | 46 +- gradesa/src/backend/db.js | 62 +- gradesa/src/backend/logging.js | 11 +- gradesa/src/backend/test/database.test.js | 21 +- gradesa/src/backend/test/migrator.js | 4 +- gradesa/src/backend/test/testdb.js | 69 +-- gradesa/src/shared/hooks/useIsMounted.js | 4 +- gradesa/src/shared/hooks/useQuery.js | 42 +- gradesa/src/shared/hooks/useRequest.js | 21 +- gradesa/vitest.config.js | 8 +- package-lock.json | 689 +++++++++++++++++++++- package.json | 19 + 29 files changed, 1049 insertions(+), 180 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100755 .husky/pre-commit create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 data/migrations/00002_add_users_table.sql create mode 100644 gradesa/.eslintrc.js delete mode 100644 gradesa/eslint.config.mjs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5bc1e98 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,39 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +tab_width = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 80 +charset = utf-8 + +[*.yaml] +indent_style = space +indent_size = 2 +tab_width = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 80 +charset = utf-8 + +[*.yml] +indent_style = space +indent_size = 2 +tab_width = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 80 +charset = utf-8 + +[*.sql] +indent_style = space +indent_size = 2 +tab_width = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index a55a7f8..dc06f9e 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -26,9 +26,13 @@ jobs: cache-dependency-path: ./gradesa/package-lock.json node-version: 22.6 cache: "npm" + - name: Install dependencies + run: npm install - name: Install client dependencies run: npm install working-directory: ./gradesa + - name: Format + run: npm run format:check - name: Download pgmigrate run: | wget --quiet https://github.com/peterldowns/pgmigrate/releases/download/v0.1.0%2Bcommit.2f6a72b/pgmigrate-linux-amd64 -O pgmigrate diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..a4fc325 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npm exec lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..540cb9d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +dist +build +node_modules +.next +.turbo diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b7b77e8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": false, + "trailingComma": "es5", + "semi": true +} \ No newline at end of file diff --git a/README.md b/README.md index d506ac0..47c8aa8 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ This should be all that's needed: - `wget https://github.com/peterldowns/pgmigrate/releases/download/v0.0.6%2Bcommit.4f90829/pgmigrate-linux-amd64 -O /usr/local/bin/pgmigrate` - `chmod +x /usr/local/bin/pgmigrate` - Docker, either of these installation options work (if you're new to docker, I recommend Docker Desktop): + - `brew install docker` + `brew install colima` (https://github.com/abiosoft/colima) - [Docker Desktop](https://www.docker.com/products/docker-desktop/) - WSL 2: Enabling `systemd` is recommended diff --git a/data/migrations/00002_add_users_table.sql b/data/migrations/00002_add_users_table.sql new file mode 100644 index 0000000..96a7e99 --- /dev/null +++ b/data/migrations/00002_add_users_table.sql @@ -0,0 +1,5 @@ +CREATE TABLE public.users ( + id bigint PRIMARY KEY NOT NULL GENERATED BY DEFAULT AS IDENTITY, + email text UNIQUE NOT NULL, + password_hash text NOT NULL +); \ No newline at end of file diff --git a/data/schema.sql b/data/schema.sql index 9c1458d..ebb06eb 100755 --- a/data/schema.sql +++ b/data/schema.sql @@ -4,3 +4,9 @@ CREATE TABLE public.pgmigrate_migrations ( execution_time_in_millis bigint NOT NULL, applied_at timestamp with time zone NOT NULL ); + +CREATE TABLE public.users ( + id bigint PRIMARY KEY NOT NULL GENERATED BY DEFAULT AS IDENTITY, + email text UNIQUE NOT NULL, + password_hash text NOT NULL +); diff --git a/gradesa/.eslintrc.js b/gradesa/.eslintrc.js new file mode 100644 index 0000000..24206ce --- /dev/null +++ b/gradesa/.eslintrc.js @@ -0,0 +1,20 @@ +module.exports = { + root: true, + extends: ["next", "prettier"], + plugins: ["eslint-plugin-react", "react-refresh", "unused-imports"], + rules: { + "turbo/no-undeclared-env-vars": 0, + "react-hooks/exhaustive-deps": 0, + "react/no-unescaped-entities": 0, + "unused-imports/no-unused-vars": [ + "error", + { + vars: "all", + varsIgnorePattern: "^_", + args: "after-used", + argsIgnorePattern: "^_", + }, + ], + }, + ignorePatterns: ["next.config.js"], +}; diff --git a/gradesa/eslint.config.mjs b/gradesa/eslint.config.mjs deleted file mode 100644 index 348c45a..0000000 --- a/gradesa/eslint.config.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import { dirname } from "path"; -import { fileURLToPath } from "url"; -import { FlatCompat } from "@eslint/eslintrc"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const compat = new FlatCompat({ - baseDirectory: __dirname, -}); - -const eslintConfig = [...compat.extends("next/core-web-vitals")]; - -export default eslintConfig; diff --git a/gradesa/package-lock.json b/gradesa/package-lock.json index 5b429d8..261b2b2 100644 --- a/gradesa/package-lock.json +++ b/gradesa/package-lock.json @@ -23,6 +23,10 @@ "@vitejs/plugin-react": "^4.3.4", "eslint": "^9", "eslint-config-next": "15.1.5", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-refresh": "^0.4.18", + "eslint-plugin-unused-imports": "^4.1.4", "jsdom": "^26.0.0", "ts-node": "^10.9.2", "vitest": "^3.0.4" @@ -3239,6 +3243,7 @@ "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -3609,6 +3614,18 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -3842,6 +3859,15 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.18.tgz", + "integrity": "sha512-IRGEoFn3OKalm3hjfolEWGqoF/jPqeEYFp+C8B0WMzwGwBMvlRDQd06kghDhF0C61uJ6WfSDhEZE/sAQjduKgw==", + "dev": true, + "peerDependencies": { + "eslint": ">=8.40" + } + }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -3868,6 +3894,21 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz", + "integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==", + "dev": true, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", @@ -7259,6 +7300,20 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/gradesa/package.json b/gradesa/package.json index 05e404e..e914b40 100644 --- a/gradesa/package.json +++ b/gradesa/package.json @@ -26,8 +26,12 @@ "@vitejs/plugin-react": "^4.3.4", "eslint": "^9", "eslint-config-next": "15.1.5", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-unused-imports": "^4.1.4", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-refresh": "^0.4.18", "jsdom": "^26.0.0", "ts-node": "^10.9.2", "vitest": "^3.0.4" } -} +} \ No newline at end of file diff --git a/gradesa/src/app/api/hello/route.js b/gradesa/src/app/api/hello/route.js index 6d8983a..edada29 100644 --- a/gradesa/src/app/api/hello/route.js +++ b/gradesa/src/app/api/hello/route.js @@ -1,12 +1,12 @@ -import { DB } from '../../../backend/db'; +import { DB } from "../../../backend/db"; export async function GET(request) { - const result = await DB.pool('SELECT now()'); + const result = await DB.pool("SELECT now()"); const query = request.nextUrl.searchParams; - const mode = query.get('mode'); - if (mode === 'error') { - return Response.json({ message: 'Error' }, { status: 500 }); + const mode = query.get("mode"); + if (mode === "error") { + return Response.json({ message: "Error" }, { status: 500 }); } const now = result.rows[0].now; - return Response.json({ message: 'Hello!', now, mode }); + return Response.json({ message: "Hello!", now, mode }); } diff --git a/gradesa/src/app/auth/layout.js b/gradesa/src/app/auth/layout.js index 77586dd..5070e78 100644 --- a/gradesa/src/app/auth/layout.js +++ b/gradesa/src/app/auth/layout.js @@ -1,11 +1,9 @@ -import './auth.css' +import "./auth.css"; export default function AuthLayout({ children }) { - return ( -
-
- {children} -
-
- ); - } + return ( +
+
{children}
+
+ ); +} diff --git a/gradesa/src/app/auth/login/page.js b/gradesa/src/app/auth/login/page.js index bd25a1d..4b142be 100644 --- a/gradesa/src/app/auth/login/page.js +++ b/gradesa/src/app/auth/login/page.js @@ -1,22 +1,24 @@ -'use client' -import { useState } from 'react'; +"use client"; +import { useState } from "react"; export default function Login() { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const handleSubmit = (event) => { event.preventDefault(); - console.log('Email:', email); - console.log('Password:', password); - } - + console.log("Email:", email); + console.log("Password:", password); + }; + return ( <>

Anmeldung

- +
- +
- + ); diff --git a/gradesa/src/app/page.js b/gradesa/src/app/page.js index b8545e8..7993036 100644 --- a/gradesa/src/app/page.js +++ b/gradesa/src/app/page.js @@ -1,12 +1,12 @@ -'use client'; +"use client"; import Image from "next/image"; import styles from "./page.module.css"; import useQuery from "@/shared/hooks/useQuery"; import { useState } from "react"; export default function Home() { - const [mode, setMode] = useState('normal'); + const [mode, setMode] = useState("normal"); - const { data, error, isLoading } = useQuery('/hello', { mode }); + const { data, error, isLoading } = useQuery("/hello", { mode }); return (
@@ -28,8 +28,8 @@ export default function Home() { {data &&

{data.message}

} DB TIME: {isLoading ?

Loading...

: data &&

{data.now}

} ERROR: {error ?

{error}

:

No error

} - - + +
{ switch (environment) { - case 'test': - case 'development': - case 'production': + case "test": + case "development": + case "production": config({ path: `${process.cwd()}/.env`, override: true }); config({ path: `${process.cwd()}/.env.${environment}`, override: true }); break; default: - throw new Error('Unsupported environment: ' + env); + throw new Error("Unsupported environment: " + env); } }; - // The following is an environment variable helper. // It's used to read environment variables from .env and .env.NODE_ENV // So instead of using process.env.DB_HOST, you can use getConfig().db.host @@ -39,7 +38,6 @@ export const getConfig = () => { return cachedConfig; }; - function getFromEnv(key) { const val = process.env[key]; if (val === undefined) { @@ -53,15 +51,15 @@ function create() { function readString(key) { const val = getFromEnv(key); - if (val === undefined || val.trim() === '') { + if (val === undefined || val.trim() === "") { errors.push(key); } - return val ?? ''; - } - - function readStringOptional(key, defaultValue) { - return (getFromEnv(key)) ?? defaultValue; + return val ?? ""; } + // Unused-vars + // function readStringOptional(key, defaultValue) { + // return getFromEnv(key) ?? defaultValue; + // } function readInt(key) { const val = getFromEnv(key); @@ -71,7 +69,7 @@ function create() { } return parseInt(val); } - + // Unused-vars function readIntOptional(key, defaultValue) { const value = getFromEnv(key); return value === undefined ? defaultValue : parseInt(value); @@ -79,16 +77,18 @@ function create() { const config = { db: { - host: readString('DB_HOST'), - port: readInt('DB_PORT'), - user: readString('DB_USER'), - password: readString('DB_PASSWORD'), - database: readString('DB_NAME'), + host: readString("DB_HOST"), + port: readInt("DB_PORT"), + user: readString("DB_USER"), + password: readString("DB_PASSWORD"), + database: readString("DB_NAME"), }, }; if (errors.length > 0) { - throw new Error(`Missing or invalid environment variables: ${errors.join(', ')}`); + throw new Error( + `Missing or invalid environment variables: ${errors.join(", ")}` + ); } return config; diff --git a/gradesa/src/backend/db.js b/gradesa/src/backend/db.js index 16f2ac6..0b005ef 100644 --- a/gradesa/src/backend/db.js +++ b/gradesa/src/backend/db.js @@ -1,26 +1,30 @@ -import { Pool } from 'pg'; -import { getConfig } from './config'; -import { isTest } from './config'; +import { Pool } from "pg"; +import { getConfig } from "./config"; +import { isTest } from "./config"; export function dbURL(config) { return `postgres://${config.user}:${config.password}@${config.host}:${config.port}/${config.database}`; } -import { logger } from './logging'; +import { logger } from "./logging"; let _pool; async function connect(pool) { - let client + let client; try { if (!pool) { - throw new Error('Pool not initialized'); + throw new Error("Pool not initialized"); } - client = await pool.connect() + client = await pool.connect(); if (!isTest) { - const row = await client.query('SELECT current_database() as database_name, now()'); - logger.info(`Connected to database ${row.rows[0].database_name} at ${row.rows[0].now.toISOString()}`); + const row = await client.query( + "SELECT current_database() as database_name, now()" + ); + logger.info( + `Connected to database ${row.rows[0].database_name} at ${row.rows[0].now.toISOString()}` + ); } } catch (err) { - logger.error('Failed to connect to database', err); + logger.error("Failed to connect to database", err); } finally { if (client) { await client.release(); @@ -38,19 +42,19 @@ async function get() { } /** -* Executes a database query using the connection pool. -* -* @param {string} text - The SQL query text to be executed. -* @param {Array} [params] - An optional array of parameters to be used in the query. -* @returns {Promise} - A promise that resolves to the result of the query. -*/ + * Executes a database query using the connection pool. + * + * @param {string} text - The SQL query text to be executed. + * @param {Array} [params] - An optional array of parameters to be used in the query. + * @returns {Promise} - A promise that resolves to the result of the query. + */ async function pool(text, params) { return (await get()).query(text, params); } /** * Executes a database transaction using the connection pool. - * + * * @param {Function} fn - A function that takes a client as an argument and returns a promise. * @returns {Promise} - A promise that resolves to the result of the transaction. */ @@ -58,12 +62,12 @@ async function transaction(fn) { const db = await get(); const client = await db.connect(); try { - await client.query('BEGIN'); + await client.query("BEGIN"); const result = await fn(client); - await client.query('COMMIT'); + await client.query("COMMIT"); return result; } catch (err) { - await client.query('ROLLBACK'); + await client.query("ROLLBACK"); throw err; } finally { await client.release(); @@ -72,7 +76,9 @@ async function transaction(fn) { async function set(config) { if (!isTest) { - throw new Error('Cannot set database in non-test environment, was ' + environment); + throw new Error( + "Cannot set database in non-test environment, was " + environment + ); } const newPool = new Pool(config); await connect(newPool); @@ -83,9 +89,9 @@ async function destroy() { if (_pool !== undefined) { if (!_pool.ended && !_pool.ending) { await _pool.end(); - logger.info('Pool destroyed'); + logger.info("Pool destroyed"); } else { - logger.warn('Pool already destroyed'); + logger.warn("Pool already destroyed"); } } _pool = undefined; @@ -96,7 +102,10 @@ async function getClient() { } async function reset() { - if (!isTest) throw new Error('Cannot reset database in non-test environment, was ' + environment); + if (!isTest) + throw new Error( + "Cannot reset database in non-test environment, was " + environment + ); if (_pool !== undefined) { await destroy(); } @@ -110,6 +119,5 @@ export const DB = { set, destroy, getClient, - reset -} - + reset, +}; diff --git a/gradesa/src/backend/logging.js b/gradesa/src/backend/logging.js index e349b67..14cc623 100644 --- a/gradesa/src/backend/logging.js +++ b/gradesa/src/backend/logging.js @@ -1,11 +1,10 @@ - // Printing these strings will change the color of the text in the console const colors = { - blue: '\x1b[34m', - red: '\x1b[31m', - yellow: '\x1b[33m', - green: '\x1b[32m', - reset: '\x1b[0m', + blue: "\x1b[34m", + red: "\x1b[31m", + yellow: "\x1b[33m", + green: "\x1b[32m", + reset: "\x1b[0m", }; export const logger = { diff --git a/gradesa/src/backend/test/database.test.js b/gradesa/src/backend/test/database.test.js index f021626..d9eb262 100644 --- a/gradesa/src/backend/test/database.test.js +++ b/gradesa/src/backend/test/database.test.js @@ -1,7 +1,7 @@ import { DB } from "../db"; -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from "vitest"; import { useTestDatabase } from "./testdb"; -describe('database', () => { +describe("database", () => { useTestDatabase(); it("Should allow for creation of tables", async () => { @@ -15,7 +15,9 @@ describe('database', () => { }); it("should not carry over changes between tests", async () => { const db = await DB.get(); - expect(async () => await db.query("SELECT * FROM TEST")).rejects.toThrow('relation "test" does not exist'); + expect(async () => await db.query("SELECT * FROM TEST")).rejects.toThrow( + 'relation "test" does not exist' + ); }); it("should get a database instance", async () => { const db = await DB.get(); @@ -23,7 +25,9 @@ describe('database', () => { }); it("should do transactions correctly", async () => { const res = await DB.transaction(async (client) => { - await client.query("CREATE table TEST (id SERIAL PRIMARY KEY, value TEXT)"); + await client.query( + "CREATE table TEST (id SERIAL PRIMARY KEY, value TEXT)" + ); await client.query("INSERT INTO TEST (value) VALUES ('test')"); return await client.query("SELECT * FROM TEST"); }); @@ -36,12 +40,14 @@ describe('database', () => { expect(result.rows.length).toBe(1); const res2 = await DB.transaction(async (client) => { - await client.query("INSERT INTO TEST (value) VALUES ('should not be inserted')"); + await client.query( + "INSERT INTO TEST (value) VALUES ('should not be inserted')" + ); await client.query("INSERT INTO TEST (value) VALUES (ERROR)"); // Invalid query return "success"; - }).catch(() => 'error'); + }).catch(() => "error"); - expect(res2).toBe('error'); + expect(res2).toBe("error"); const result2 = await DB.pool("SELECT * FROM TEST"); expect(result2).toBeDefined(); @@ -49,4 +55,3 @@ describe('database', () => { expect(result2.rows[0].value).toBe("test"); }); }); - diff --git a/gradesa/src/backend/test/migrator.js b/gradesa/src/backend/test/migrator.js index ce52400..6f26a31 100644 --- a/gradesa/src/backend/test/migrator.js +++ b/gradesa/src/backend/test/migrator.js @@ -78,6 +78,8 @@ export class Migrator { } get migrationsDir() { - return path.join(__dirname, "../../../../data", "migrations").replace("dist/", ""); + return path + .join(__dirname, "../../../../data", "migrations") + .replace("dist/", ""); } } diff --git a/gradesa/src/backend/test/testdb.js b/gradesa/src/backend/test/testdb.js index 930359e..eb1876a 100644 --- a/gradesa/src/backend/test/testdb.js +++ b/gradesa/src/backend/test/testdb.js @@ -23,8 +23,8 @@ export function useTestDatabase({ // Init new DB baseDB = new Pool({ max: 1, - ...conf - }) + ...conf, + }); } catch (e) { throw new Error( `Failed to connect to test database, is docker-compose running? : ${e}` @@ -45,7 +45,7 @@ export function useTestDatabase({ // 4. Create testDB // We set the testDB to use the new database - await DB.set({ max: 1, ...testDBConfig }) + await DB.set({ max: 1, ...testDBConfig }); testDB = await DB.get(); }); @@ -72,11 +72,7 @@ export function useTestDatabase({ let template; -async function getOrCreateTemplate( - pool, - conf, - migrator -) { +async function getOrCreateTemplate(pool, conf, migrator) { const hash = await migrator.hash(); if (template !== undefined) { @@ -117,9 +113,11 @@ async function ensureUser(db) { } ensureUserCalled = true; - await withLock(db, 'testdb-user', async (client) => { + await withLock(db, "testdb-user", async (client) => { const roleExists = ( - await client.query(`SELECT EXISTS (SELECT from pg_catalog.pg_roles WHERE rolname = '${TestUser}')`) + await client.query( + `SELECT EXISTS (SELECT from pg_catalog.pg_roles WHERE rolname = '${TestUser}')` + ) ).rows[0]; if (roleExists.exists) { @@ -127,31 +125,30 @@ async function ensureUser(db) { } await client.query(`CREATE ROLE ${TestUser}`); - await client.query(`ALTER ROLE ${TestUser} WITH LOGIN PASSWORD '${TestPassword}' NOSUPERUSER NOCREATEDB NOCREATEROLE`); + await client.query( + `ALTER ROLE ${TestUser} WITH LOGIN PASSWORD '${TestPassword}' NOSUPERUSER NOCREATEDB NOCREATEROLE` + ); }); } // createInstance creates a new database instance using the template database -async function createInstance( - pool, - template -) { +async function createInstance(pool, template) { const testConf = { ...template.conf }; testConf.database = `testdb_tpl_${template.hash}_inst_${randomID()}`; - await pool.query(`CREATE DATABASE ${testConf.database} WITH TEMPLATE ${template.conf.database} OWNER ${testConf.user}`); + await pool.query( + `CREATE DATABASE ${testConf.database} WITH TEMPLATE ${template.conf.database} OWNER ${testConf.user}` + ); return testConf; } - -async function ensureTemplate( - client, - migrator, - state -) { +async function ensureTemplate(client, migrator, state) { // If the template database already exists, and is marked as a template, // there is no more work to be done. - const templateExists = await client.query(`SELECT EXISTS (SELECT FROM pg_database WHERE datname = $1 AND datistemplate = true)`, [state.conf.database]); + const templateExists = await client.query( + `SELECT EXISTS (SELECT FROM pg_database WHERE datname = $1 AND datistemplate = true)`, + [state.conf.database] + ); if (templateExists.rows[0].exists) { return; } @@ -160,13 +157,18 @@ async function ensureTemplate( // template, there was a failure at some point during the creation process // so it needs to be deleted. await client.query(`DROP DATABASE IF EXISTS ${state.conf.database}`); - await client.query(`CREATE DATABASE ${state.conf.database} OWNER ${state.conf.user}`) + await client.query( + `CREATE DATABASE ${state.conf.database} OWNER ${state.conf.user}` + ); - await migrator.migrate(state.conf) + await migrator.migrate(state.conf); // Finalize the creation of the template by marking it as a // template. - await client.query(`UPDATE pg_database SET datistemplate = true WHERE datname=$1`, [state.conf.database]); + await client.query( + `UPDATE pg_database SET datistemplate = true WHERE datname=$1`, + [state.conf.database] + ); } const idPrefix = "sessionlock-"; @@ -180,32 +182,25 @@ function id(name) { return hexInt | 0; // Convert to 32-bit integer } - // The withLock function ensures that a block of code runs with a database lock. // This prevents other code from running the same block at the same time. -async function withLock( - pool, - lockName, - cb -) { - const lockID = id(lockName); // Create a lock ID +async function withLock(pool, lockName, cb) { + const lockID = id(lockName); // Create a lock ID const client = await pool.connect(); try { await client.query(`SELECT pg_advisory_lock(${lockID})`); // Acquire the lock. await cb(client); // Run the function with the lock. } catch (e) { - logger.error('failed to lock', e); + logger.error("failed to lock", e); } finally { await client.query(`SELECT pg_advisory_unlock(${lockID})`); // Release the lock. await client.release(); } } - - function randomID() { const hash = createHash("md5"); hash.update(randomBytes(4)); return hash.digest("hex"); -} \ No newline at end of file +} diff --git a/gradesa/src/shared/hooks/useIsMounted.js b/gradesa/src/shared/hooks/useIsMounted.js index 022fe9b..d4f0ddd 100644 --- a/gradesa/src/shared/hooks/useIsMounted.js +++ b/gradesa/src/shared/hooks/useIsMounted.js @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef } from "react"; /** * @description This hook is used to check if the component is mounted. @@ -13,4 +13,4 @@ export const useIsMounted = () => { }; }, []); return isMounted.current; -}; \ No newline at end of file +}; diff --git a/gradesa/src/shared/hooks/useQuery.js b/gradesa/src/shared/hooks/useQuery.js index 86c5ee8..1bebc44 100644 --- a/gradesa/src/shared/hooks/useQuery.js +++ b/gradesa/src/shared/hooks/useQuery.js @@ -1,24 +1,25 @@ -import axios from 'axios'; -import { stringify } from 'qs'; -import { useState, useEffect, useRef, useMemo } from 'react'; -import { useIsMounted } from './useIsMounted'; +import axios from "axios"; +import { stringify } from "qs"; +import { useState, useEffect, useRef, useMemo } from "react"; +import { useIsMounted } from "./useIsMounted"; -const baseUrl = process.env.NEXT_ENV === 'production' ? 'https://tbd.com' : 'http://localhost:3000'; +const baseUrl = + process.env.NEXT_ENV === "production" + ? "https://tbd.com" + : "http://localhost:3000"; export const axiosInstance = axios.create({ baseURL: `${baseUrl}/api`, }); - /** * @description This function converts an object to a query string. * @param {Object} obj - The object to convert to a query string. * @returns {string} - The query string. */ const objectToQueryString = (obj) => { - return stringify(obj, { encodeValuesOnly: true, }); + return stringify(obj, { encodeValuesOnly: true }); }; - const defaultConfig = { enabled: true, refetchBackoffMax: 10_000, @@ -59,11 +60,13 @@ const useQuery = (url, params, config) => { } setIsLoading(true); const fullUrl = queryString ? `${url}?${queryString}` : url; - const response = await axiosInstance.get(fullUrl, { signal: abortCtrl.signal }); + const response = await axiosInstance.get(fullUrl, { + signal: abortCtrl.signal, + }); if (isMounted && !!response.data) { setData(response.data); } else { - throw new Error('Failed to fetch data'); + throw new Error("Failed to fetch data"); } }; @@ -78,7 +81,11 @@ const useQuery = (url, params, config) => { const maxBackoff = config.refetchBackoffMax; try { setIsLoading(true); - while (currentAttemptRef.current < maxAttempts && isMounted && !abortCtrl.signal.aborted) { + while ( + currentAttemptRef.current < maxAttempts && + isMounted && + !abortCtrl.signal.aborted + ) { try { await makeGetRequest(url, queryString, abortCtrl); // If succeeded, exit the loop @@ -96,11 +103,13 @@ const useQuery = (url, params, config) => { maxBackoff ); // Set the exponent for the next attempt - // Set to 1 for the first attempt to make the initial + // Set to 1 for the first attempt to make the initial // backoff equal to the base backoff exponent = memoizedConfig.refetchBackoffExponent; // Wait for the backoff duration - await new Promise(resolve => setTimeout(resolve, currentBackoffRef.current)); + await new Promise((resolve) => + setTimeout(resolve, currentBackoffRef.current) + ); } } } catch (error) { @@ -125,7 +134,12 @@ const useQuery = (url, params, config) => { } let abortCtrl = new AbortController(); (async () => { - await retryWithBackoff(url, memoizedQueryString, memoizedConfig, abortCtrl); + await retryWithBackoff( + url, + memoizedQueryString, + memoizedConfig, + abortCtrl + ); })(); return () => { abortCtrl.abort(); diff --git a/gradesa/src/shared/hooks/useRequest.js b/gradesa/src/shared/hooks/useRequest.js index 20eb738..605af5b 100644 --- a/gradesa/src/shared/hooks/useRequest.js +++ b/gradesa/src/shared/hooks/useRequest.js @@ -1,9 +1,9 @@ -import { axiosInstance } from './useQuery'; +import { axiosInstance } from "./useQuery"; const defaultConfig = { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, }; @@ -16,10 +16,11 @@ export const useRequest = () => { * @param {Object} config - The config of the request. * @returns {Object} - The response from the server. */ - return (url, body, config) => axiosInstance.request({ - url, - data: body, - ...defaultConfig, - ...config, - }); -} \ No newline at end of file + return (url, body, config) => + axiosInstance.request({ + url, + data: body, + ...defaultConfig, + ...config, + }); +}; diff --git a/gradesa/vitest.config.js b/gradesa/vitest.config.js index 76a7212..ef6ac31 100644 --- a/gradesa/vitest.config.js +++ b/gradesa/vitest.config.js @@ -1,9 +1,9 @@ -import { defineConfig } from 'vitest/config' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], test: { - environment: 'jsdom', + environment: "jsdom", }, -}) \ No newline at end of file +}); diff --git a/package-lock.json b/package-lock.json index 209470d..0ef525e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,694 @@ "": { "name": "ohtu-german-learning-website", "version": "1.0.0", - "license": "ISC" + "license": "ISC", + "devDependencies": { + "husky": "^8.0.3", + "lint-staged": "^13.3.0", + "prettier": "^3.2.4" + } + }, + "node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "dev": true, + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", + "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lint-staged": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.3.0.tgz", + "integrity": "sha512-mPRtrYnipYYv1FEE134ufbWpeggNTo+O/UPzngoaKzbzHAthvR55am+8GfHTnqNRQVRRrYQLGW9ZyUoD7DsBHQ==", + "dev": true, + "dependencies": { + "chalk": "5.3.0", + "commander": "11.0.0", + "debug": "4.3.4", + "execa": "7.2.0", + "lilconfig": "2.1.0", + "listr2": "6.6.1", + "micromatch": "4.0.5", + "pidtree": "0.6.0", + "string-argv": "0.3.2", + "yaml": "2.3.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/listr2": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-6.6.1.tgz", + "integrity": "sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg==", + "dev": true, + "dependencies": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^5.0.1", + "rfdc": "^1.3.0", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/log-update": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", + "integrity": "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==", + "dev": true, + "dependencies": { + "ansi-escapes": "^5.0.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^5.0.0", + "strip-ansi": "^7.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "dev": true, + "engines": { + "node": ">= 14" + } } } } diff --git a/package.json b/package.json index 408e940..5db1dd6 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,32 @@ { "name": "ohtu-german-learning-website", "version": "1.0.0", + "packageManager": "npm@10.2.4", "description": "GRADESA is a freely accessible website where language learners at language centres as well as university students of German studies can autonomously work on their German grammar skills and knowledge.", "scripts": { "db:migrate": "./data/migrate.sh", "db:reset": "./data/reset-db.sh", + "prepare": "husky install", "dev": "npm run --prefix gradesa dev", + "format:check": "prettier --check \"**/*.{js,jsx,md,yml}\"", + "format:fix": "prettier --write \"**/*.{js,jsx,md,yml}\"", "test": "echo \"Error: no test specified\" && exit 1" }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "*.{js,ts,jsx,tsx,md,yml}": [ + "prettier --write" + ] + }, + "devDependencies": { + "lint-staged": "^13.3.0", + "prettier": "^3.2.4", + "husky": "^8.0.3" + }, "author": "", "license": "ISC" } \ No newline at end of file