From 1bbdfef316b4cf94ae3c56373816f4833983c980 Mon Sep 17 00:00:00 2001 From: Birk Jonathan Ramstad Date: Tue, 13 Oct 2026 14:37:47 +0200 Subject: [PATCH 1/4] feat: initialize backend and PostgreSQL connection --- backend/.env.example | 2 + backend/package-lock.json | 745 ++++++++++++++++++++++++++++++++++++++ backend/package.json | 23 ++ backend/src/db/pool.ts | 20 + backend/tsconfig.json | 18 + 5 files changed, 808 insertions(+) create mode 100644 backend/.env.example create mode 100644 backend/package-lock.json create mode 100644 backend/package.json create mode 100644 backend/src/db/pool.ts create mode 100644 backend/tsconfig.json diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..465c818 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,2 @@ +# PostgreSQL-tilkobling. Kopier til .env og tilpass verdien. +DATABASE_URL=postgresql://bruker:passord@localhost:5432/verktoyutlan diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..cab0ece --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,745 @@ +{ + "name": "t36-verktoyutlan-backend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "t36-verktoyutlan-backend", + "version": "0.0.0", + "dependencies": { + "dotenv": "^17.2.3", + "pg": "^8.16.3" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@types/pg": "^8.15.6", + "tsx": "^4.20.6", + "typescript": "~6.0.0" + }, + "engines": { + "node": "^22.22.2 || >=24.15.0", + "npm": ">=10" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.1", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.1", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz", + "integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz", + "integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz", + "integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..05cccda --- /dev/null +++ b/backend/package.json @@ -0,0 +1,23 @@ +{ + "name": "t36-verktoyutlan-backend", + "private": true, + "version": "0.0.0", + "type": "module", + "engines": { + "node": "^22.22.2 || >=24.15.0", + "npm": ">=10" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "dotenv": "^17.2.3", + "pg": "^8.16.3" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@types/pg": "^8.15.6", + "tsx": "^4.20.6", + "typescript": "~6.0.0" + } +} diff --git a/backend/src/db/pool.ts b/backend/src/db/pool.ts new file mode 100644 index 0000000..207a120 --- /dev/null +++ b/backend/src/db/pool.ts @@ -0,0 +1,20 @@ +import 'dotenv/config'; + +import pg from 'pg'; + +const { Pool } = pg; + +function requireDatabaseUrl(): string { + const url = process.env.DATABASE_URL; + if (!url) { + throw new Error( + 'DATABASE_URL er ikke satt. Kopier backend/.env.example til backend/.env.', + ); + } + return url; +} + +/** Delt pool for PostgreSQL. Opprettes ved første import. */ +export const pool = new Pool({ + connectionString: requireDatabaseUrl(), +}); diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..c344441 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "skipLibCheck": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "esModuleInterop": true + }, + "include": ["src/**/*.ts"] +} From b161026c49c33f0d5a0524671b2dc5cfb3f02feb Mon Sep 17 00:00:00 2001 From: Birk Jonathan Ramstad Date: Tue, 13 Oct 2026 14:42:50 +0200 Subject: [PATCH 2/4] feat: add listings database migration --- backend/.node-pg-migraterc.json | 4 + .../1791895214550_create-listings-table.ts | 48 +++ backend/package-lock.json | 303 ++++++++++++++++++ backend/package.json | 2 + backend/tsconfig.json | 2 +- 5 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 backend/.node-pg-migraterc.json create mode 100644 backend/migrations/1791895214550_create-listings-table.ts diff --git a/backend/.node-pg-migraterc.json b/backend/.node-pg-migraterc.json new file mode 100644 index 0000000..0486989 --- /dev/null +++ b/backend/.node-pg-migraterc.json @@ -0,0 +1,4 @@ +{ + "migrations-dir": "migrations", + "migration-file-language": "ts" +} diff --git a/backend/migrations/1791895214550_create-listings-table.ts b/backend/migrations/1791895214550_create-listings-table.ts new file mode 100644 index 0000000..8b11642 --- /dev/null +++ b/backend/migrations/1791895214550_create-listings-table.ts @@ -0,0 +1,48 @@ +import type { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; + +export const shorthands: ColumnDefinitions | undefined = undefined; + +const LISTING_CATEGORIES = [ + 'powerTools', + 'handTools', + 'garden', + 'painting', + 'measuring', + 'cleaning', +] as const; + +const LISTING_CONDITIONS = ['likeNew', 'good', 'worn'] as const; + +function sqlInList(values: readonly string[]): string { + return values.map((value) => `'${value}'`).join(', '); +} + +export async function up(pgm: MigrationBuilder): Promise { + pgm.createTable('listings', { + id: { type: 'text', primaryKey: true }, + title: { type: 'text', notNull: true }, + category: { type: 'text', notNull: true }, + description: { type: 'text', notNull: true }, + area: { type: 'text', notNull: true }, + daily_price: { type: 'integer', notNull: true }, + condition: { type: 'text', notNull: true }, + accessories: { type: 'text[]', notNull: true, default: '{}' }, + image_url: { type: 'text', notNull: false }, + }); + + pgm.addConstraint('listings', 'listings_category_check', { + check: `category IN (${sqlInList(LISTING_CATEGORIES)})`, + }); + + pgm.addConstraint('listings', 'listings_condition_check', { + check: `condition IN (${sqlInList(LISTING_CONDITIONS)})`, + }); + + pgm.addConstraint('listings', 'listings_daily_price_check', { + check: 'daily_price >= 0', + }); +} + +export async function down(pgm: MigrationBuilder): Promise { + pgm.dropTable('listings'); +} diff --git a/backend/package-lock.json b/backend/package-lock.json index cab0ece..94770eb 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,6 +14,7 @@ "devDependencies": { "@types/node": "^24.10.1", "@types/pg": "^8.15.6", + "node-pg-migrate": "^9.0.0", "tsx": "^4.20.6", "typescript": "~6.0.0" }, @@ -486,6 +487,70 @@ "pg-types": "^2.2.0" } }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -498,6 +563,13 @@ "url": "https://dotenvx.com" } }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", @@ -540,6 +612,16 @@ "@esbuild/win32-x64": "0.28.2" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -555,6 +637,137 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/node-pg-migrate": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/node-pg-migrate/-/node-pg-migrate-9.0.0.tgz", + "integrity": "sha512-lp5+UZx1KKgOz/y0h6BYGPuJ5wVlZTgiBPGXCGoX6Bs6X9LtiIhrI4V2yW4mPwnJBfptVq0KtLQtXHcoEKOyig==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "~13.0.0", + "jiti": "~2.7.0", + "yargs": "~18.0.0" + }, + "bin": { + "node-pg-migrate": "bin/node-pg-migrate.js" + }, + "engines": { + "node": ">=20.11.0" + }, + "peerDependencies": { + "@types/pg": ">=6.0.0 <9.0.0", + "pg": ">=4.3.0 <9.0.0" + }, + "peerDependenciesMeta": { + "@types/pg": { + "optional": true + } + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pg": { "version": "8.23.1", "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz", @@ -692,6 +905,40 @@ "node": ">= 10.x" } }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/tsx": { "version": "4.23.15", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", @@ -732,6 +979,24 @@ "dev": true, "license": "MIT" }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -740,6 +1005,44 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } } } } diff --git a/backend/package.json b/backend/package.json index 05cccda..9326e37 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,6 +8,7 @@ "npm": ">=10" }, "scripts": { + "migrate": "node-pg-migrate up --envPath .env", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -17,6 +18,7 @@ "devDependencies": { "@types/node": "^24.10.1", "@types/pg": "^8.15.6", + "node-pg-migrate": "^9.0.0", "tsx": "^4.20.6", "typescript": "~6.0.0" } diff --git a/backend/tsconfig.json b/backend/tsconfig.json index c344441..974eb81 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -14,5 +14,5 @@ "isolatedModules": true, "esModuleInterop": true }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "migrations/**/*.ts"] } From 2870e43b9384b4e4689ed89cfdaee381c02be997 Mon Sep 17 00:00:00 2001 From: Birk Jonathan Ramstad Date: Tue, 13 Oct 2026 14:52:42 +0200 Subject: [PATCH 3/4] feat: add listing seed and backend documentation --- README.md | 52 ++++++++++ backend/.env.example | 3 + backend/package.json | 1 + backend/src/scripts/seed.ts | 64 ++++++++++++ backend/src/seed/listingImageUrl.ts | 12 +++ backend/src/seed/listings.ts | 146 ++++++++++++++++++++++++++++ backend/src/types/listing.ts | 11 +++ 7 files changed, 289 insertions(+) create mode 100644 backend/src/scripts/seed.ts create mode 100644 backend/src/seed/listingImageUrl.ts create mode 100644 backend/src/seed/listings.ts create mode 100644 backend/src/types/listing.ts diff --git a/README.md b/README.md index 59790e2..2351996 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,51 @@ npm ci # installer avhengigheter nøyaktig etter package-lock.json npm run dev # start utviklingsserver på http://localhost:5173 ``` +## Backend og database + +Backend ligger i `backend/` og bruker PostgreSQL. GraphQL kommer i senere +issues; foreløpig finnes migrasjon og seed for eksempelannonsene. + +### Installasjon + +```bash +cd backend +npm ci +cp .env.example .env +``` + +Rediger `.env` og sett `DATABASE_URL` til PostgreSQL-instansen du bruker +(lokalt eller på gruppens VM). Valgfritt: `PUBLIC_APP_BASE` styrer bildestier +i seed (standard `/project2`). + +### Migrasjon og seed + +Kjør migrasjon før seed, begge fra `backend/`: + +```bash +npm run migrate +npm run seed +``` + +`npm run seed` er idempotent: eksisterende rader med samme `id` hoppes over +(`ON CONFLICT DO NOTHING`). Kjør seed på nytt uten duplikater. + +Seed fyller de 10 annonsene fra `src/data/mockListings.ts` (id `listing-001` til +`listing-010`). Bildestier lagres som offentlige URL-er under +`/project2/assets/listings/` med filnavn som i `src/assets/listings/`, uten at +backend importerer SVG-filer. + +### Verifisere data + +Etter migrasjon og seed: + +```bash +psql "$DATABASE_URL" -c "SELECT id, title, daily_price FROM listings ORDER BY id;" +``` + +Forvent **10 rader**. Kjør `npm run seed` en gang til og sjekk at antall +fortsatt er 10. + ## Kommandoer | Kommando | Hva den gjør | @@ -143,6 +188,8 @@ npm run dev # start utviklingsserver på http://localhost:5173 | `npm run format` | Formaterer alle filer med Prettier | | `npm run format:check` | Sjekker formatering uten å endre filer | +Backend (`cd backend`): `npm run migrate`, `npm run seed`, `npm run typecheck`. + ESLint finner feil i koden, mens Prettier bestemmer formateringen. `eslint-config-prettier` ligger sist i `eslint.config.js` og slår av ESLint-regler som ellers ville kollidert med Prettier. Byggfiler, @@ -154,6 +201,11 @@ kontroller som er gjort i nettleser, står i [docs/testing.md](docs/testing.md). ## Mappestruktur ``` +backend/ + migrations/ Versjonerte PostgreSQL-migrasjoner + src/db/ Tilkoblingspool + src/scripts/ Seed og andre engangsskript + src/seed/ Seed-data for eksempelannonser src/ types/ Felles datatyper, for eksempel Listing data/ Mockdata. Byttes med GraphQL-backend senere diff --git a/backend/.env.example b/backend/.env.example index 465c818..bdcc789 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,2 +1,5 @@ # PostgreSQL-tilkobling. Kopier til .env og tilpass verdien. DATABASE_URL=postgresql://bruker:passord@localhost:5432/verktoyutlan + +# Valgfritt: rotsti når frontend er deployet (standard /project2). +# PUBLIC_APP_BASE=/project2 diff --git a/backend/package.json b/backend/package.json index 9326e37..ba81005 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,6 +9,7 @@ }, "scripts": { "migrate": "node-pg-migrate up --envPath .env", + "seed": "tsx --env-file=.env src/scripts/seed.ts", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/backend/src/scripts/seed.ts b/backend/src/scripts/seed.ts new file mode 100644 index 0000000..a796bba --- /dev/null +++ b/backend/src/scripts/seed.ts @@ -0,0 +1,64 @@ +import { pool } from '../db/pool.js'; +import { seedListings } from '../seed/listings.js'; + +const INSERT_SQL = ` + INSERT INTO listings ( + id, + title, + category, + description, + area, + daily_price, + condition, + accessories, + image_url + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT (id) DO NOTHING +`; + +async function seed(): Promise { + if (seedListings.length !== 10) { + throw new Error( + `Forventet 10 seed-annonser, fant ${seedListings.length}.`, + ); + } + + let inserted = 0; + let skipped = 0; + + for (const listing of seedListings) { + const result = await pool.query(INSERT_SQL, [ + listing.id, + listing.title, + listing.category, + listing.description, + listing.area, + listing.dailyPrice, + listing.condition, + listing.accessories, + listing.imageUrl, + ]); + + if (result.rowCount === 1) { + inserted += 1; + } else { + skipped += 1; + } + } + + const countResult = await pool.query<{ count: string }>( + 'SELECT COUNT(*)::text AS count FROM listings', + ); + const total = Number(countResult.rows[0]?.count ?? 0); + + console.info( + `Seed fullført: ${inserted} nye, ${skipped} hoppet over (finnes fra før). Totalt ${total} rader i listings.`, + ); +} + +try { + await seed(); +} finally { + await pool.end(); +} diff --git a/backend/src/seed/listingImageUrl.ts b/backend/src/seed/listingImageUrl.ts new file mode 100644 index 0000000..531f8f7 --- /dev/null +++ b/backend/src/seed/listingImageUrl.ts @@ -0,0 +1,12 @@ +/** + * Offentlig bildesti for annonser etter deploy under /project2. + * Filnavn tilsvarer SVG-ene i frontend src/assets/listings/. + */ + +const DEFAULT_APP_BASE = '/project2'; + +export function listingImageUrl(filename: string): string { + const base = process.env.PUBLIC_APP_BASE ?? DEFAULT_APP_BASE; + const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base; + return `${normalizedBase}/assets/listings/${filename}`; +} diff --git a/backend/src/seed/listings.ts b/backend/src/seed/listings.ts new file mode 100644 index 0000000..c5db1ed --- /dev/null +++ b/backend/src/seed/listings.ts @@ -0,0 +1,146 @@ +/** + * Seed-data for eksempelannonsene. Innholdet tilsvarer src/data/mockListings.ts + * uten å importere frontend eller SVG-filer. + */ + +import type { ToolCategory, ToolCondition } from '../types/listing.js'; +import { listingImageUrl } from './listingImageUrl.js'; + +export interface SeedListing { + id: string; + title: string; + category: ToolCategory; + description: string; + area: string; + dailyPrice: number; + condition: ToolCondition; + accessories: string[]; + imageUrl: string | null; +} + +export const seedListings: SeedListing[] = [ + { + id: 'listing-001', + title: 'Drill', + category: 'powerTools', + description: + 'Batteridrill som passer til å skru opp hyller og montere møbler.', + area: 'Moholt', + dailyPrice: 100, + condition: 'good', + accessories: ['2 batterier', 'Lader', 'Bitssett'], + imageUrl: listingImageUrl('drill.svg'), + }, + { + id: 'listing-002', + title: 'Slagdrill med borsett for betong og murvegger', + category: 'powerTools', + description: + 'Kraftig slagdrill for boring i betong, mur og tegl. Egner seg til å henge opp tunge hyller, TV-braketter og kjøkkenskap. Borsett fra 6 til 12 mm følger med. Bruk hørselvern ved lengre arbeid.', + area: 'Lade', + dailyPrice: 150, + condition: 'likeNew', + accessories: ['Borsett for betong', 'Koffert', 'Dybdestopper'], + imageUrl: listingImageUrl('drill.svg'), + }, + { + id: 'listing-003', + title: 'Fogsvans', + category: 'handTools', + description: 'Vanlig håndsag for tre. Bladet er skarpt.', + area: 'Gløshaugen', + dailyPrice: 0, + condition: 'good', + accessories: [], + imageUrl: listingImageUrl('handsaw.svg'), + }, + { + id: 'listing-004', + title: 'Verktøykasse med hammer, skrutrekkere og tenger', + category: 'handTools', + description: + 'Grunnleggende verktøy for småreparasjoner i hybelen. Kassen har hammer, flere skrutrekkere, nebbtang, kombinasjonstang og målebånd.', + area: 'Singsaker', + dailyPrice: 50, + condition: 'worn', + accessories: ['Målebånd', 'Unbrakonøkler'], + imageUrl: null, + }, + { + id: 'listing-005', + title: 'Spade', + category: 'garden', + description: 'Solid spade til hagearbeid og planting.', + area: 'Byåsen', + dailyPrice: 0, + condition: 'worn', + accessories: [], + imageUrl: listingImageUrl('spade.svg'), + }, + { + id: 'listing-006', + title: 'Hagespade og rive', + category: 'garden', + description: + 'Spade og rive som lånes ut sammen. Fint til å gjøre klar et lite bed eller rake løv om høsten.', + area: 'Tiller', + dailyPrice: 40, + condition: 'good', + accessories: ['Rive', 'Arbeidshansker'], + imageUrl: listingImageUrl('spade.svg'), + }, + { + id: 'listing-007', + title: 'Malerrulle og brett', + category: 'painting', + description: + 'Rulle og malebrett for vegger. Rullene er vasket og klare til bruk.', + area: 'Møllenberg', + dailyPrice: 0, + condition: 'likeNew', + accessories: ['2 ruller', 'Malebrett', 'Teleskopskaft'], + imageUrl: listingImageUrl('paint-roller.svg'), + }, + { + id: 'listing-008', + title: + 'Komplett malersett til en liten leilighet: ruller, pensler, dekkplast og tape', + category: 'painting', + description: + 'Alt du trenger for å male om en hybel eller en liten leilighet, bortsett fra maling. Settet har ruller i to størrelser, pensler til kanter, dekkplast til gulv og møbler og maskeringstape. Rengjør ruller og pensler før levering.', + area: 'Midtbyen', + dailyPrice: 120, + condition: 'good', + accessories: [ + 'Ruller i to størrelser', + '3 pensler', + 'Dekkplast', + 'Maskeringstape', + ], + imageUrl: listingImageUrl('paint-roller.svg'), + }, + { + id: 'listing-009', + title: 'Laservater', + category: 'measuring', + description: + 'Krysslaser for å henge bilder og hyller rett. Rekker omtrent 15 meter innendørs.', + area: 'Heimdal', + dailyPrice: 80, + condition: 'likeNew', + accessories: ['Stativ', 'Batterier'], + imageUrl: null, + }, + { + id: 'listing-010', + title: 'Tepperenser', + category: 'cleaning', + description: + 'Rensemaskin for tepper og møbeltrekk. Nyttig ved utflytting. Rensemiddel følger ikke med.', + area: 'Nardo', + dailyPrice: 250, + condition: 'good', + accessories: ['Slange', '2 munnstykker'], + imageUrl: null, + }, +]; diff --git a/backend/src/types/listing.ts b/backend/src/types/listing.ts new file mode 100644 index 0000000..bfcf729 --- /dev/null +++ b/backend/src/types/listing.ts @@ -0,0 +1,11 @@ +/** Speiler src/types/listing.ts for backend uten avhengighet til frontend. */ + +export type ToolCategory = + | 'powerTools' + | 'handTools' + | 'garden' + | 'painting' + | 'measuring' + | 'cleaning'; + +export type ToolCondition = 'likeNew' | 'good' | 'worn'; From 686162c3ca263687e3b78e7387022fb9a1e62ed1 Mon Sep 17 00:00:00 2001 From: Birk Jonathan Ramstad Date: Tue, 13 Oct 2026 19:23:37 +0200 Subject: [PATCH 4/4] docs: finalize backend setup and fix formatting --- README.md | 36 +++++++++++++++++++++++------------- backend/src/scripts/seed.ts | 4 +--- backend/src/types/listing.ts | 7 +------ docs/ki-bruk.md | 20 ++++++++++++++------ 4 files changed, 39 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 2351996..09fee18 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,13 @@ privatpersoner i Trondheim. Bygget med React, TypeScript og Vite. ## Om prototypen -Dette er første innlevering: en klikkbar prototype som viser idéen og hvordan -løsningen er tenkt brukt. Den har ingen backend, database eller innlogging. +Første innlevering var en klikkbar **frontend-prototype** som viser idéen og +hvordan løsningen er tenkt brukt. Katalogen henter fortsatt data fra mock i +nettleseren, og leie/utleie er demo uten innlogging. + +**Datalag (innlevering 2):** PostgreSQL, migrasjon og seed ligger i `backend/`. +GraphQL og kobling fra React kommer i senere issues. Se +[Backend og database](#backend-og-database). ### Idé @@ -44,9 +49,9 @@ annonser viser for eksempel «150 kr/døgn», mens gratis annonser viser Ingen forespørsel blir sendt, og verktøyet blir ikke reservert. - **Utleieskjemaet er en demo.** Knappen heter «Legg ut verktøy (demo)». Annonsen blir ikke lagret og vises ikke i katalogen. -- **Ingenting lagres.** Det finnes ingen backend, database eller innlogging. - Søk og filtre virker bare på eksempelannonsene i nettleseren, og det som - er fylt ut i skjemaene, forsvinner når siden lastes inn på nytt. +- **Appen lagrer ikke leie eller nye annonser.** Utleie- og leieforespørsel er + demo. Søk og filtre i katalogen går mot mockdata i nettleseren. Seed i + databasen er for backend og senere API, ikke noe katalogen leser ennå. ### Data @@ -65,15 +70,13 @@ I den ferdige løsningen ligger dataene i en database: | Vurdering | Leietakerens vurdering etter et gjennomført leieforhold | Annonser, bookinger og vurderinger er de brukergenererte dataene som lagres. -For å teste søk og blaing i store resultatsett fyller vi databasen med -genererte eksempelannonser via et seed-skript. Om vi henter produktdata fra en -ekstern kilde, er ikke avklart. +Eksempelannonsene seedes i PostgreSQL via `backend/` (samme innhold som mock). +Om vi henter produktdata fra en ekstern kilde, er ikke avklart. ### Planlagt videre -- **Backend og database:** En egen GraphQL-backend i Node og TypeScript med - PostgreSQL. Begge kjører direkte på gruppens VM, uten Docker. Appen blir - tilgjengelig via Apache under `/project2`, og backenden på port 3001. +- **GraphQL og VM:** API i Node og TypeScript, deploy på gruppens VM uten + Docker. Appen under Apache `/project2`, backend på port 3001. - **Søk i hele databasen:** Søk, filtre og sortering skjer på serveren, før resultatet deles opp i sider. Katalogen får blaing med «Forrige» og «Neste», og endrede valg starter på første side. Søket venter til brukeren @@ -159,10 +162,17 @@ npm run seed (`ON CONFLICT DO NOTHING`). Kjør seed på nytt uten duplikater. Seed fyller de 10 annonsene fra `src/data/mockListings.ts` (id `listing-001` til -`listing-010`). Bildestier lagres som offentlige URL-er under -`/project2/assets/listings/` med filnavn som i `src/assets/listings/`, uten at +`listing-010`). `image_url` lagres som offentlige stier under +`/project2/assets/listings/` (filnavn som i `src/assets/listings/`), uten at backend importerer SVG-filer. +**Bilder og deploy:** `npm run build` i frontend pakker SVG-ene inn i +JavaScript-bundelen. Den lager **ikke** løse filer på +`/project2/assets/listings/`. For at seedede URL-er skal fungere når klienten +senere leser fra databasen, må SVG-ene serveres **separat** på den stien, for +eksempel ved at Apache peker `/project2/assets/listings/` til en kopi av +`src/assets/listings/` på VM. Dette er ikke en del av Vite-bygget alene. + ### Verifisere data Etter migrasjon og seed: diff --git a/backend/src/scripts/seed.ts b/backend/src/scripts/seed.ts index a796bba..604f21e 100644 --- a/backend/src/scripts/seed.ts +++ b/backend/src/scripts/seed.ts @@ -19,9 +19,7 @@ const INSERT_SQL = ` async function seed(): Promise { if (seedListings.length !== 10) { - throw new Error( - `Forventet 10 seed-annonser, fant ${seedListings.length}.`, - ); + throw new Error(`Forventet 10 seed-annonser, fant ${seedListings.length}.`); } let inserted = 0; diff --git a/backend/src/types/listing.ts b/backend/src/types/listing.ts index bfcf729..5f992b0 100644 --- a/backend/src/types/listing.ts +++ b/backend/src/types/listing.ts @@ -1,11 +1,6 @@ /** Speiler src/types/listing.ts for backend uten avhengighet til frontend. */ export type ToolCategory = - | 'powerTools' - | 'handTools' - | 'garden' - | 'painting' - | 'measuring' - | 'cleaning'; + 'powerTools' | 'handTools' | 'garden' | 'painting' | 'measuring' | 'cleaning'; export type ToolCondition = 'likeNew' | 'good' | 'worn'; diff --git a/docs/ki-bruk.md b/docs/ki-bruk.md index 5650b33..f86081a 100644 --- a/docs/ki-bruk.md +++ b/docs/ki-bruk.md @@ -2,34 +2,39 @@ ## Verktøy + | Verktøy | Modell | Brukes til | | ----------- | --------------- | -------------------------------------------------- | | Claude Code | Claude Opus 5.5 | Planlegging, issues, kode, tester og dokumentasjon | + ## Slik bruker vi KI - KI får en avgrenset oppgave, normalt ett issue med akseptansekriterier. - KI skriver kode i samme stil som resten av repoet og bruker de felles - komponentene, typene og hjelpefunksjonene. +komponentene, typene og hjelpefunksjonene. - Alt KI lager, går gjennom vanlig pull request og må godkjennes av et - gruppemedlem før merge, på samme måte som annen kode. +gruppemedlem før merge, på samme måte som annen kode. + + ## Slik kontrollerer vi resultatene 1. **Automatiske kontroller:** typecheck, lint, format:check, test og build - må gå uten feil. + må gå uten feil. 2. **Nettleser:** endringen prøves i appen, også på smal skjerm og med - tastatur. + tastatur. 3. **Akseptansekriterier:** hvert kriterium i issuet sjekkes mot det som er - laget. + laget. 4. **Gjennomgang i pull request:** et annet gruppemedlem leser koden og kan be - om endringer. + om endringer. Feil som KI har gjort og som blir oppdaget, føres i loggen under, slik at vi ser hva vi bør være oppmerksomme på. ## Logg + | Issue | Hva KI gjorde | Kontroll og funn | | ----- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | – | Gikk gjennom repoet mot arbeidsplanen og foreslo de resterende issuene til innlevering 1 | Issuene ble lest og opprettet av gruppen | @@ -39,3 +44,6 @@ ser hva vi bør være oppmerksomme på. | #7 | Satte opp Vitest og skrev 79 tester, og kontrollerte ren installasjon, bredder og tastatur | En test sjekket påkrevde felt med `toBeInvalid()` og ville bestått uansett. Byttet til `aria-invalid`. Nettleserkontrollene var automatiserte, se [testing.md](testing.md) | | #3 | Skrev prosjektstandard, maler for issues og pull requests og dette dokumentet | Standarden ble sjekket mot commit-historikken og reglene i repoet | | #21 | Kjørte sluttkontrollen før innlevering 1 og rettet det den fant | Fant at `engines` tillot Node-versjoner som `jsdom` ikke støtter, og at favicon manglet. Begge er rettet. PR-er, review og merge til `main` gjøres av gruppen | +| #31 | Kjørte en kontroll på backend oppsettet | Sluttkontroll fant Prettier-feil og at seed-bilder ikke følger av Vite-build; dokumentasjon oppdatert. Migrate/seed og typecheck verifisert. PR og merge gjøres av gruppen | + +