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 <evisuri@cubbli22-gold.local>
Co-authored-by: DoebelnAtz <axel.ej.adlercreutz@gmail.com>
This commit is contained in:
EmilVisuri
2025-02-05 16:20:18 +02:00
committed by GitHub
co-authored by Visuri Emil A DoebelnAtz
parent 2d4ce31cdf
commit 84da650a8f
29 changed files with 1049 additions and 180 deletions
+39
View File
@@ -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
+4
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
node_modules
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm exec lint-staged
+5
View File
@@ -0,0 +1,5 @@
dist
build
node_modules
.next
.turbo
+5
View File
@@ -0,0 +1,5 @@
{
"singleQuote": false,
"trailingComma": "es5",
"semi": true
}
+1
View File
@@ -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
@@ -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
);
+6
View File
@@ -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
);
+20
View File
@@ -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"],
};
-14
View File
@@ -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;
+55
View File
@@ -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",
+4
View File
@@ -26,6 +26,10 @@
"@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"
+6 -6
View File
@@ -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 });
}
+2 -4
View File
@@ -1,11 +1,9 @@
import './auth.css'
import "./auth.css";
export default function AuthLayout({ children }) {
return (
<div className="auth-container">
<div className="auth-box">
{children}
</div>
<div className="auth-box">{children}</div>
</div>
);
}
+16 -10
View File
@@ -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 (
<>
<h1 className="auth-title">Anmeldung</h1>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label className="form-label" htmlFor="email">E-Mail-Adresse</label>
<label className="form-label" htmlFor="email">
E-Mail-Adresse
</label>
<input
className="form-input"
type="email"
@@ -29,7 +31,9 @@ export default function Login() {
</div>
<div className="form-group">
<label className="form-label" htmlFor="password">Passwort</label>
<label className="form-label" htmlFor="password">
Passwort
</label>
<input
className="form-input"
type="password"
@@ -41,7 +45,9 @@ export default function Login() {
/>
</div>
<button type="submit" className="form-button">Einloggen</button>
<button type="submit" className="form-button">
Einloggen
</button>
</form>
</>
);
+5 -5
View File
@@ -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 (
<div className={styles.page}>
@@ -28,8 +28,8 @@ export default function Home() {
{data && <p>{data.message}</p>}
DB TIME: {isLoading ? <p>Loading...</p> : data && <p>{data.now}</p>}
ERROR: {error ? <p>{error}</p> : <p>No error</p>}
<button onClick={() => setMode('error')}>Error</button>
<button onClick={() => setMode('normal')}>Normal</button>
<button onClick={() => setMode("error")}>Error</button>
<button onClick={() => setMode("normal")}>Normal</button>
<div className={styles.ctas}>
<a
className={styles.primary}
+23 -23
View File
@@ -1,10 +1,10 @@
import { config } from 'dotenv';
import { config } from "dotenv";
export const environment = process.env.NODE_ENV;
export const isTest = environment === 'test';
export const isDevelopment = environment === 'development';
export const isProduction = environment === 'production';
export const isTest = environment === "test";
export const isDevelopment = environment === "development";
export const isProduction = environment === "production";
// Loads .env and .env.NODE_ENV (e.g. .env.development)
// Shared environment variables are in .env
@@ -12,18 +12,17 @@ export const isProduction = environment === 'production';
// NextJS defaults to 'development' when running npm run dev
export const initConfig = async () => {
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;
+28 -20
View File
@@ -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();
@@ -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,
};
+5 -6
View File
@@ -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 = {
+13 -8
View File
@@ -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");
});
});
+3 -1
View File
@@ -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/", "");
}
}
+30 -35
View File
@@ -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,14 +182,9 @@ 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
) {
async function withLock(pool, lockName, cb) {
const lockID = id(lockName); // Create a lock ID
const client = await pool.connect();
@@ -195,15 +192,13 @@ async function withLock(
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));
+1 -1
View File
@@ -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.
+27 -13
View File
@@ -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
@@ -100,7 +107,9 @@ const useQuery = (url, params, config) => {
// 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();
+6 -5
View File
@@ -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({
return (url, body, config) =>
axiosInstance.request({
url,
data: body,
...defaultConfig,
...config,
});
}
};
+4 -4
View File
@@ -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",
},
})
});
+688 -1
View File
@@ -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"
}
}
}
}
+19
View File
@@ -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"
}