diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml new file mode 100644 index 0000000..3543b87 --- /dev/null +++ b/.github/workflows/backend-ci.yml @@ -0,0 +1,99 @@ +name: Backend CI Pipeline + +on: + push: + branches: [ main ] + paths: + - 'backend/**' + pull_request: + branches: [ main ] + paths: + - 'backend/**' + +jobs: + backend-ci: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:17-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: 1234 + POSTGRES_DB: test_db + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: | + backend/node_modules + backend/.bun + ~/.bun/install/cache + backend/prisma/generated + key: ${{ runner.os }}-bun-${{ hashFiles('backend/bun.lock', 'backend/package.json', 'backend/tsconfig.json') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install dependencies + working-directory: ./backend + run: bun install + + - name: Generate Prisma client + working-directory: ./backend + run: bunx prisma generate + env: + DATABASE_URL: postgresql://postgres:1234@localhost:5432/test_db + + - name: Code quality check (Biome) + working-directory: ./backend + run: bun run check + + - name: Type checking + working-directory: ./backend + run: bun run typecheck + + - name: Run database migrations + working-directory: ./backend + run: bunx prisma migrate deploy + env: + DATABASE_URL: postgresql://postgres:1234@localhost:5432/test_db + + - name: Security audit + working-directory: ./backend + run: bun audit + + - name: Build application + working-directory: ./backend + run: bun run build + + - name: Build verification + working-directory: ./backend + run: | + # Verify the built application can start without errors + timeout 10s bun dist/index.js || true + + - name: Upload build artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: backend-build-artifacts + path: | + backend/dist/ + backend/prisma/generated/ + retention-days: 7 \ No newline at end of file diff --git a/.gitignore b/.gitignore index b91d3f8..01423fc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules/ # Build and distribution output /dist +backend/dist/ /dist-ssr /build /out diff --git a/.husky/pre-commit b/.husky/pre-commit index e530fdf..ec6aa55 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,2 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -bunx lint-staged +#!/usr/bin/env sh +npx --no -- lint-staged diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..22e7bbb --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ +build/ + +# Environment variables +.env +.env.* + +# Logs +*.log + +# Coverage +coverage/ + +# Temporary files +*.tmp +*.temp \ No newline at end of file diff --git a/backend/biome.json b/backend/biome.json index 313ff9f..0bdabf3 100644 --- a/backend/biome.json +++ b/backend/biome.json @@ -3,10 +3,11 @@ "vcs": { "enabled": true, "clientKind": "git", - "useIgnoreFile": true + "useIgnoreFile": false }, "files": { - "ignoreUnknown": true + "ignoreUnknown": true, + "includes": ["src/**"] }, "formatter": { "enabled": true, @@ -19,6 +20,12 @@ "correctness": { "noUnusedImports": "error", "noUnusedVariables": "error" + }, + "style": { + "noNonNullAssertion": "off" + }, + "suspicious": { + "noExplicitAny": "off" } } }, @@ -26,9 +33,7 @@ "formatter": { "quoteStyle": "double" }, - "globals": [ - "Bun" - ] + "globals": ["Bun"] }, "assist": { "enabled": true, diff --git a/backend/bruno/bruno.json b/backend/bruno/bruno.json index 99c0fa5..a4bc18d 100644 --- a/backend/bruno/bruno.json +++ b/backend/bruno/bruno.json @@ -2,8 +2,5 @@ "version": "1", "name": "formEngine", "type": "collection", - "ignore": [ - "node_modules", - ".git" - ] -} \ No newline at end of file + "ignore": ["node_modules", ".git"] +} diff --git a/backend/bun.lock b/backend/bun.lock index 909fb0c..1b4f195 100644 --- a/backend/bun.lock +++ b/backend/bun.lock @@ -8,47 +8,51 @@ "@elysiajs/cors": "^1.4.1", "@prisma/adapter-pg": "^7.3.0", "@prisma/client": "^7.3.0", - "better-auth": "^1.4.12", - "elysia": "^1.0.0", - "pg": "^8.17.2", - "pino": "^10.1.1", + "better-auth": "^1.4.18", + "elysia": "^1.4.22", + "pg": "^8.18.0", + "pino": "^10.3.0", }, "devDependencies": { - "@biomejs/biome": "^2.3.11", + "@biomejs/biome": "^2.3.13", "@types/bun": "latest", "@types/pg": "^8.16.0", "pino-pretty": "^13.1.3", - "prisma": "7.3.0", - "typescript": "^5.3.0", + "prisma": "^7.3.0", + "typescript": "^5.9.3", }, }, }, + "overrides": { + "hono": "^4.11.7", + "lodash": "^4.17.23", + }, "packages": { - "@better-auth/core": ["@better-auth/core@1.4.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-VfqZwMAEl9rnGx092BIZ2Q5z8rt7jjN2OAbvPqehufSKZGmh8JsdtZRBMl/CHQir9bwi2Ev0UF4+7TQp+DXEMg=="], + "@better-auth/core": ["@better-auth/core@1.4.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.4.12", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.12" } }, "sha512-4q504Og42PzkUbZjXDt+FyeYaS0WZmAlEOC3nbBCZDObTVCRUnGgJW52B2maJ7BCVvAQgBGLEeQmQzU5+63J0A=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.4.18", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.18" } }, "sha512-e5rDF8S4j3Um/0LIVATL2in9dL4lfO2fr2v1Wio4qTMRbfxqnUDTa+6SZtwdeJrbc4O+a3c+IyIpjG9Q/6GpfQ=="], "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], - "@biomejs/biome": ["@biomejs/biome@2.3.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.11", "@biomejs/cli-darwin-x64": "2.3.11", "@biomejs/cli-linux-arm64": "2.3.11", "@biomejs/cli-linux-arm64-musl": "2.3.11", "@biomejs/cli-linux-x64": "2.3.11", "@biomejs/cli-linux-x64-musl": "2.3.11", "@biomejs/cli-win32-arm64": "2.3.11", "@biomejs/cli-win32-x64": "2.3.11" }, "bin": { "biome": "bin/biome" } }, "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ=="], + "@biomejs/biome": ["@biomejs/biome@2.3.13", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.13", "@biomejs/cli-darwin-x64": "2.3.13", "@biomejs/cli-linux-arm64": "2.3.13", "@biomejs/cli-linux-arm64-musl": "2.3.13", "@biomejs/cli-linux-x64": "2.3.13", "@biomejs/cli-linux-x64-musl": "2.3.13", "@biomejs/cli-win32-arm64": "2.3.13", "@biomejs/cli-win32-x64": "2.3.13" }, "bin": { "biome": "bin/biome" } }, "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-AGr8OoemT/ejynbIu56qeil2+F2WLkIjn2d8jGK1JkchxnMUhYOfnqc9sVzcRxpG9Ycvw4weQ5sprRvtb7Yhcw=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-TUdDCSY+Eo/EHjhJz7P2GnWwfqet+lFxBZzGHldrvULr59AgahamLs/N85SC4+bdF86EhqDuuw9rYLvLFWWlXA=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.13", "", { "os": "linux", "cpu": "x64" }, "sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.13", "", { "os": "linux", "cpu": "x64" }, "sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.13", "", { "os": "win32", "cpu": "x64" }, "sha512-trDw2ogdM2lyav9WFQsdsfdVy1dvZALymRpgmWsvSez0BJzBjulhOT/t+wyKeh3pZWvwP3VMs1SoOKwO3wecMQ=="], "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], @@ -112,7 +116,7 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], "@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], @@ -124,11 +128,11 @@ "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], - "better-auth": ["better-auth@1.4.12", "", { "dependencies": { "@better-auth/core": "1.4.12", "@better-auth/telemetry": "1.4.12", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-FsFMnWgk+AGrxsIGbpWLCibgYcbm6uNhPHln3ohXFDXSRa0gk39Beuh54Q+x6ml2qYodF0snxf/tPtDpBI/JiA=="], + "better-auth": ["better-auth@1.4.18", "", { "dependencies": { "@better-auth/core": "1.4.18", "@better-auth/telemetry": "1.4.18", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg=="], - "better-call": ["better-call@1.1.7", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-6gaJe1bBIEgVebQu/7q9saahVzvBsGaByEnE8aDVncZEDiJO7sdNB28ot9I6iXSbR25egGmmZ6aIURXyQHRraQ=="], + "better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="], - "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], @@ -166,7 +170,7 @@ "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], - "elysia": ["elysia@1.4.21", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.6", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-bGSbPSGnkWbO0qUDKS5Q+6iEewBdMmIiJ8F0li4djZ6WjpixUQouOzePYscG1Lemdv6pZpFi1YPfI/kjeq2voA=="], + "elysia": ["elysia@1.4.22", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.6", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Q90VCb1RVFxnFaRV0FDoSylESQQLWgLHFmWciQJdX9h3b2cSasji9KWEUvaJuy/L9ciAGg4RAhUVfsXHg5K2RQ=="], "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], @@ -202,7 +206,7 @@ "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], - "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], + "hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="], "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], @@ -224,7 +228,7 @@ "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -260,11 +264,11 @@ "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], - "pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="], + "pg": ["pg@8.18.0", "", { "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ=="], "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], - "pg-connection-string": ["pg-connection-string@2.10.1", "", {}, "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw=="], + "pg-connection-string": ["pg-connection-string@2.11.0", "", {}, "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ=="], "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], @@ -276,7 +280,7 @@ "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], - "pino": ["pino@10.1.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-3qqVfpJtRQUCAOs4rTOEwLH6mwJJ/CSAlbis8fKOiMzTtXh0HN/VLsn3UWVTJ7U8DsWmxeNon2IpGb+wORXH4g=="], + "pino": ["pino@10.3.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA=="], "pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], @@ -380,6 +384,8 @@ "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "@prisma/adapter-pg/pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="], + "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="], "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="], @@ -389,5 +395,7 @@ "pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@prisma/adapter-pg/pg/pg-connection-string": ["pg-connection-string@2.10.1", "", {}, "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw=="], } } diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..f91e7b3 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,39 @@ +services: + + postgres: + image: postgres:17-alpine + container_name: formEngine-postgres + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: 1234 + POSTGRES_DB: postgres + ports: + - "5432:5432" + restart: on-failure + networks: + - app_network + volumes: + - postgres_data:/var/lib/postgresql/data + + drizzle-gate: + image: ghcr.io/drizzle-team/gateway:latest + container_name: formEngine-drizzle-gateway + environment: + DATABASE_URL: postgresql://postgres:1234@postgres:5432/postgres + STORE_PATH: /app + restart: on-failure + depends_on: + - postgres + ports: + - "4983:4983" + volumes: + - drizzle_gateway_data:/app + networks: + - app_network + +volumes: + postgres_data: + drizzle_gateway_data: + +networks: + app_network: diff --git a/backend/package.json b/backend/package.json index 4f7dfed..f5e23bb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,24 +6,29 @@ "scripts": { "dev": "bun --watch src/index.ts", "check": "biome check .", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "build": "bun build src/index.ts --outdir dist --target bun --minify" }, "dependencies": { "@elysiajs/cors": "^1.4.1", "@prisma/adapter-pg": "^7.3.0", "@prisma/client": "^7.3.0", - "better-auth": "^1.4.12", - "elysia": "^1.0.0", - "pg": "^8.17.2", - "pino": "^10.1.1" + "better-auth": "^1.4.18", + "elysia": "^1.4.22", + "pg": "^8.18.0", + "pino": "^10.3.0" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", + "@biomejs/biome": "^2.3.13", "@types/bun": "latest", "@types/pg": "^8.16.0", "pino-pretty": "^13.1.3", - "prisma": "7.3.0", - "typescript": "^5.3.0" + "prisma": "^7.3.0", + "typescript": "^5.9.3" + }, + "resolutions": { + "hono": "^4.11.7", + "lodash": "^4.17.23" }, "module": "src/index.ts" } diff --git a/backend/src/api/auth/index.ts b/backend/src/api/auth/index.ts index 794bae7..6710bc2 100644 --- a/backend/src/api/auth/index.ts +++ b/backend/src/api/auth/index.ts @@ -4,7 +4,7 @@ import { prisma } from "../../db/prisma"; export const auth = betterAuth({ database: prismaAdapter(prisma, { - provider: "postgresql" + provider: "postgresql", }), user: { diff --git a/backend/src/api/auth/requireAuth.ts b/backend/src/api/auth/requireAuth.ts index e0b8a36..6f8115d 100644 --- a/backend/src/api/auth/requireAuth.ts +++ b/backend/src/api/auth/requireAuth.ts @@ -1,6 +1,6 @@ -import { Elysia } from "elysia"; -import { auth } from "./index"; +import type { Elysia } from "elysia"; import { logger } from "../../logger/"; +import { auth } from "./index"; interface User { id: string; diff --git a/backend/src/api/auth/routes.ts b/backend/src/api/auth/routes.ts index d64ba02..9c4b2dc 100644 --- a/backend/src/api/auth/routes.ts +++ b/backend/src/api/auth/routes.ts @@ -1,5 +1,4 @@ import { Elysia } from "elysia"; import { auth } from "./index"; -export const authRoutes = new Elysia() - .mount(auth.handler) +export const authRoutes = new Elysia().mount(auth.handler); diff --git a/backend/src/api/form-fields/controller.ts b/backend/src/api/form-fields/controller.ts index 057aa58..dc093c4 100644 --- a/backend/src/api/form-fields/controller.ts +++ b/backend/src/api/form-fields/controller.ts @@ -1,10 +1,15 @@ -import { prisma } from "../../db/prisma" -import { logger } from "../../logger/" -import { GetAllFieldsContext, CreateFieldContext, UpdateFieldContext, DeleteFieldContext } from "../../types/form-fields" +import { prisma } from "../../db/prisma"; +import { logger } from "../../logger/"; +import type { + CreateFieldContext, + DeleteFieldContext, + GetAllFieldsContext, + UpdateFieldContext, +} from "../../types/form-fields"; export async function getAllFields({ params, set }: GetAllFieldsContext) { const formExists = await prisma.form.count({ - where: { id: params.formId } + where: { id: params.formId }, }); if (formExists === 0) { @@ -13,7 +18,7 @@ export async function getAllFields({ params, set }: GetAllFieldsContext) { return { success: false, message: "Form not found", - data: [] + data: [], }; } @@ -26,54 +31,60 @@ export async function getAllFields({ params, set }: GetAllFieldsContext) { fieldType: true, validation: true, prevFieldId: true, - nextField: true + nextField: true, }, - where: { formId: params.formId } - }) - + where: { formId: params.formId }, + }); if (fields.length === 0) { - logger.info(`No fields found for formId: ${params.formId}`) + logger.info(`No fields found for formId: ${params.formId}`); return { success: true, message: "No forms fields found", - data: [] - } + data: [], + }; } - logger.info(`Fetched all fields for formId: ${params.formId}, fieldCount: ${fields.length}`) + logger.info( + `Fetched all fields for formId: ${params.formId}, fieldCount: ${fields.length}`, + ); return { success: true, message: "All form fields fetched successfully", - data: fields - } + data: fields, + }; } -export async function createField({ params, body, set, user }: CreateFieldContext) { +export async function createField({ + params, + body, + set, + user, +}: CreateFieldContext) { const form = await prisma.form.findFirst({ where: { id: params.formId, - ownerId: user.id - } - }) + ownerId: user.id, + }, + }); if (!form) { - set.status = 404 + set.status = 404; return { success: false, - message: "Form not found" - } + message: "Form not found", + }; } - const field = await prisma.$transaction(async (tx) => { + const field = await prisma.$transaction(async (tx: any) => { // 1. If we are inserting after a specific field (prevFieldId provided) if (body.prevFieldId) { // Fetch the previous field to see if it has a next field const prevField = await tx.formFields.findUnique({ - where: { id: body.prevFieldId } - }) + where: { id: body.prevFieldId }, + }); if (!prevField) { - throw new Error("Previous field not found") + throw new Error("Previous field not found"); } // 2. Create the new field @@ -88,30 +99,30 @@ export async function createField({ params, body, set, user }: CreateFieldContex validation: body.validation ?? undefined, prevFieldId: body.prevFieldId, nextField: prevField.nextField, // Inherit the link - formId: params.formId - } - }) + formId: params.formId, + }, + }); // 3. Update the previous field to point to the new field await tx.formFields.update({ where: { id: body.prevFieldId }, - data: { nextField: newField.id } - }) + data: { nextField: newField.id }, + }); // 4. If there was a next field, update it to point back to the new field if (prevField.nextField) { - // We can't query by `id` directly if `nextField` is just a string without relation, + // We can't query by `id` directly if `nextField` is just a string without relation, // but typically we can update the row where id matches the string. await tx.formFields.update({ where: { id: prevField.nextField }, - data: { prevFieldId: newField.id } - }) + data: { prevFieldId: newField.id }, + }); } - return newField - } - - // Fallback: If no prevFieldId is provided, we assume it's the first field + return newField; + } + + // Fallback: If no prevFieldId is provided, we assume it's the first field // or simply creating a field without links yet. return await tx.formFields.create({ data: { @@ -120,34 +131,39 @@ export async function createField({ params, body, set, user }: CreateFieldContex fieldValueType: body.fieldValueType, fieldType: body.fieldType, validation: body.validation ?? undefined, - formId: params.formId - } - }) - }) + formId: params.formId, + }, + }); + }); - logger.info(`Created field ${field.id} for form ${params.formId}`) + logger.info(`Created field ${field.id} for form ${params.formId}`); return { success: true, message: "Field created successfully", - data: field - } + data: field, + }; } -export async function updateField({ params, body, set, user }: UpdateFieldContext) { +export async function updateField({ + params, + body, + set, + user, +}: UpdateFieldContext) { const field = await prisma.formFields.findUnique({ where: { id: params.id }, - include: { form: true } - }) + include: { form: true }, + }); if (!field) { - set.status = 404 - return { success: false, message: "Field not found" } + set.status = 404; + return { success: false, message: "Field not found" }; } if (field.form.ownerId !== user.id) { - set.status = 403 - return { success: false, message: "Unauthorized" } + set.status = 403; + return { success: false, message: "Unauthorized" }; } const updatedField = await prisma.formFields.update({ @@ -158,58 +174,57 @@ export async function updateField({ params, body, set, user }: UpdateFieldContex fieldValueType: body.fieldValueType, fieldType: body.fieldType, validation: body.validation ?? undefined, - } - }) + }, + }); - logger.info(`Updated field ${updatedField.id}`) + logger.info(`Updated field ${updatedField.id}`); return { success: true, message: "Field updated successfully", - data: updatedField - } + data: updatedField, + }; } export async function deleteField({ params, set, user }: DeleteFieldContext) { const field = await prisma.formFields.findUnique({ where: { id: params.id }, - include: { form: true } - }) + include: { form: true }, + }); if (!field) { - set.status = 404 - return { success: false, message: "Field not found" } + set.status = 404; + return { success: false, message: "Field not found" }; } if (field.form.ownerId !== user.id) { - set.status = 403 - return { success: false, message: "Unauthorized" } + set.status = 403; + return { success: false, message: "Unauthorized" }; } - await prisma.$transaction(async (tx) => { + await prisma.$transaction(async (tx: any) => { // 1. Link Previous to Next if (field.prevFieldId) { await tx.formFields.update({ where: { id: field.prevFieldId }, - data: { nextField: field.nextField } - }) + data: { nextField: field.nextField }, + }); } // 2. Link Next to Previous if (field.nextField) { await tx.formFields.update({ where: { id: field.nextField }, - data: { prevFieldId: field.prevFieldId } - }) + data: { prevFieldId: field.prevFieldId }, + }); } // 3. Delete the field await tx.formFields.delete({ - where: { id: params.id } - }) - }) + where: { id: params.id }, + }); + }); - logger.info(`Deleted field ${params.id}`) - return { success: true, message: "Field deleted successfully" } + logger.info(`Deleted field ${params.id}`); + return { success: true, message: "Field deleted successfully" }; } - diff --git a/backend/src/api/form-fields/routes.ts b/backend/src/api/form-fields/routes.ts index 1d649de..75b31fd 100644 --- a/backend/src/api/form-fields/routes.ts +++ b/backend/src/api/form-fields/routes.ts @@ -1,7 +1,17 @@ -import { requireAuth } from "../auth/requireAuth"; -import { getAllFields, createField, updateField, deleteField } from "./controller" -import { getAllFieldsDTO, createFieldDTO, updateFieldDTO, deleteFieldDTO } from "../../types/form-fields"; import { Elysia } from "elysia"; +import { + createFieldDTO, + deleteFieldDTO, + getAllFieldsDTO, + updateFieldDTO, +} from "../../types/form-fields"; +import { requireAuth } from "../auth/requireAuth"; +import { + createField, + deleteField, + getAllFields, + updateField, +} from "./controller"; export const formFieldRoutes = new Elysia({ prefix: "/fields" }) .use(requireAuth) @@ -9,4 +19,3 @@ export const formFieldRoutes = new Elysia({ prefix: "/fields" }) .post("/:formId", createField, createFieldDTO) .put("/:id", updateField, updateFieldDTO) .delete("/:id", deleteField, deleteFieldDTO); - diff --git a/backend/src/api/forms/controller.ts b/backend/src/api/forms/controller.ts index 0cdfe5c..3140ebc 100644 --- a/backend/src/api/forms/controller.ts +++ b/backend/src/api/forms/controller.ts @@ -1,6 +1,12 @@ -import { prisma } from "../../db/prisma" -import { logger } from "../../logger/" -import { Context, CreateFormContext, GetFormByIdContext, UpdateFormContext, DeleteFormContext } from "../../types/forms" +import { prisma } from "../../db/prisma"; +import { logger } from "../../logger/"; +import type { + Context, + CreateFormContext, + DeleteFormContext, + GetFormByIdContext, + UpdateFormContext, +} from "../../types/forms"; export async function getAllForms({ user }: Context) { const forms = await prisma.form.findMany({ @@ -8,26 +14,29 @@ export async function getAllForms({ user }: Context) { id: true, title: true, isPublished: true, - createdAt: true + createdAt: true, }, - where: { ownerId: user.id } - }) + where: { ownerId: user.id }, + }); if (forms.length === 0) { - logger.info("No forms found for user", { userId: user.id }) + logger.info("No forms found for user", { userId: user.id }); return { success: true, message: "No forms found", - data: [] - } + data: [], + }; } - logger.info("Fetched all forms for user", { userId: user.id, formCount: forms.length }) + logger.info("Fetched all forms for user", { + userId: user.id, + formCount: forms.length, + }); return { success: true, message: "All forms fetched successfully", - data: forms - } + data: forms, + }; } export async function createForm({ user, body }: CreateFormContext) { @@ -36,14 +45,17 @@ export async function createForm({ user, body }: CreateFormContext) { title: body.title, description: body.description, ownerId: user.id, - } - }) - logger.info("Created new form for user", { userId: user.id, formId: form.id }) + }, + }); + logger.info("Created new form for user", { + userId: user.id, + formId: form.id, + }); return { success: true, message: "Form created successfully", - data: form - } + data: form, + }; } export async function getFormById({ user, params, set }: GetFormByIdContext) { @@ -51,41 +63,46 @@ export async function getFormById({ user, params, set }: GetFormByIdContext) { where: { id: params.id, ownerId: user.id, - } - }) + }, + }); if (!form) { - set.status = 404 + set.status = 404; return { success: false, message: "Form not found", - } + }; } - logger.info("Fetched form for user", { userId: user.id, formId: form.id }) + logger.info("Fetched form for user", { userId: user.id, formId: form.id }); return { success: true, message: "Form fetched successfully", - data: form - } + data: form, + }; } -export async function updateForm({ user, params, body, set }: UpdateFormContext) { - // We use findFirst first to ensure ownership and existence before updating, +export async function updateForm({ + user, + params, + body, + set, +}: UpdateFormContext) { + // We use findFirst first to ensure ownership and existence before updating, // as prisma.update throws if not found. const existing = await prisma.form.findFirst({ where: { id: params.id, ownerId: user.id, - } - }) + }, + }); if (!existing) { - set.status = 404 + set.status = 404; return { success: false, message: "Form not found", - } + }; } const form = await prisma.form.update({ @@ -95,15 +112,15 @@ export async function updateForm({ user, params, body, set }: UpdateFormContext) data: { title: body.title, description: body.description, - } - }) + }, + }); - logger.info("Updated form for user", { userId: user.id, formId: form.id }) + logger.info("Updated form for user", { userId: user.id, formId: form.id }); return { success: true, message: "Form updated successfully", - data: form - } + data: form, + }; } export async function deleteForm({ user, params, set }: DeleteFormContext) { @@ -111,21 +128,24 @@ export async function deleteForm({ user, params, set }: DeleteFormContext) { where: { id: params.id, ownerId: user.id, - } - }) + }, + }); if (form.count === 0) { - logger.warn("Attempted to delete non-existent form", { userId: user.id, formId: params.id }) - set.status = 404 + logger.warn("Attempted to delete non-existent form", { + userId: user.id, + formId: params.id, + }); + set.status = 404; return { success: false, message: "Form not found", - } + }; } - logger.info("Deleted form for user", { userId: user.id, formId: params.id }) + logger.info("Deleted form for user", { userId: user.id, formId: params.id }); return { success: true, message: "Form deleted successfully", - } + }; } diff --git a/backend/src/api/forms/routes.ts b/backend/src/api/forms/routes.ts index 41ea025..62f5aa0 100644 --- a/backend/src/api/forms/routes.ts +++ b/backend/src/api/forms/routes.ts @@ -1,7 +1,18 @@ -import { requireAuth } from "../auth/requireAuth"; -import { getAllForms, createForm, getFormById, updateForm, deleteForm } from "./controller"; -import { createFormDTO, getFormByIdDTO, updateFormDTO, deleteFormDTO } from "../../types/forms"; import { Elysia } from "elysia"; +import { + createFormDTO, + deleteFormDTO, + getFormByIdDTO, + updateFormDTO, +} from "../../types/forms"; +import { requireAuth } from "../auth/requireAuth"; +import { + createForm, + deleteForm, + getAllForms, + getFormById, + updateForm, +} from "./controller"; export const formRoutes = new Elysia({ prefix: "/forms" }) .use(requireAuth) @@ -10,5 +21,3 @@ export const formRoutes = new Elysia({ prefix: "/forms" }) .get("/:id", getFormById, getFormByIdDTO) .put("/:id", updateForm, updateFormDTO) .delete("/:id", deleteForm, deleteFormDTO); - - diff --git a/backend/src/db/prisma.ts b/backend/src/db/prisma.ts index 53f5880..e9c5a7c 100644 --- a/backend/src/db/prisma.ts +++ b/backend/src/db/prisma.ts @@ -1,6 +1,6 @@ import "dotenv/config"; -import { PrismaClient } from "@prisma/client"; import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaClient } from "@prisma/client"; const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL!, diff --git a/backend/src/index.ts b/backend/src/index.ts index 0829450..08d1ecb 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,9 +1,9 @@ -import { Elysia } from "elysia"; import { cors } from "@elysiajs/cors"; -import { logger } from "./logger/index"; -import { authRoutes } from "./api/auth/routes" -import { formRoutes } from "./api/forms/routes" +import { Elysia } from "elysia"; +import { authRoutes } from "./api/auth/routes"; import { formFieldRoutes } from "./api/form-fields/routes"; +import { formRoutes } from "./api/forms/routes"; +import { logger } from "./logger/index"; const app = new Elysia() .use(cors()) @@ -41,7 +41,8 @@ const app = new Elysia() success: false, message: "Internal server error", }; - }).get("/", () => "🦊 Elysia server started") + }) + .get("/", () => "🦊 Elysia server started") .use(authRoutes) .use(formRoutes) .use(formFieldRoutes); diff --git a/backend/src/types/form-fields.ts b/backend/src/types/form-fields.ts index 538bdf2..b3944c9 100644 --- a/backend/src/types/form-fields.ts +++ b/backend/src/types/form-fields.ts @@ -1,4 +1,4 @@ -import { t, type Static } from "elysia"; +import { type Static, t } from "elysia"; export interface Context { user: { id: string }; @@ -8,7 +8,7 @@ export interface Context { export const getAllFieldsDTO = { params: t.Object({ formId: t.String({ - format: "uuid" + format: "uuid", }), }), }; @@ -29,9 +29,11 @@ export const createFieldDTO = { fieldValueType: t.String(), fieldType: t.String(), validation: t.Optional(t.Any()), - prevFieldId: t.Optional(t.String({ - format: "uuid", - })), + prevFieldId: t.Optional( + t.String({ + format: "uuid", + }), + ), }), }; diff --git a/backend/src/types/forms.ts b/backend/src/types/forms.ts index 7179b59..8b60905 100644 --- a/backend/src/types/forms.ts +++ b/backend/src/types/forms.ts @@ -1,4 +1,4 @@ -import { t, type Static } from "elysia"; +import { type Static, t } from "elysia"; export interface Context { user: { id: string }; @@ -19,7 +19,7 @@ export interface CreateFormContext extends Context { export const getFormByIdDTO = { params: t.Object({ id: t.String({ - format: "uuid" + format: "uuid", }), }), }; @@ -31,7 +31,7 @@ export interface GetFormByIdContext extends Context { export const updateFormDTO = { params: t.Object({ id: t.String({ - format: "uuid" + format: "uuid", }), }), body: t.Object({ @@ -48,10 +48,10 @@ export interface UpdateFormContext extends Context { export const deleteFormDTO = { params: t.Object({ id: t.String({ - format: "uuid" + format: "uuid", }), }), -} +}; export interface DeleteFormContext extends Context { params: Static; diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 2ca47bb..83d6b5f 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -79,7 +79,7 @@ /* Type Checking */ "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "noImplicitAny": false, /* Allow explicit 'any' types where needed. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ diff --git a/package.json b/package.json index 2fa5fbf..86d2307 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,10 @@ "backend/**/*.{ts,js,json}": [ "bunx biome check --write", "bunx biome check" + ], + ".github/**/*.yml": [ + "bunx biome check --write", + "bunx biome check" ] }, "dependencies": {