From d5eb0603628a194bf83928a1cf4e7c86cddbd19e Mon Sep 17 00:00:00 2001 From: Victor Date: Sun, 3 May 2026 04:48:39 +0000 Subject: [PATCH] migrate: replace pg with Prisma ORM - Add prisma/schema.prisma with Project/Task models, enums, and relations - Create src/lib/db.ts singleton Prisma client - Refactor all 5 API routes to use Prisma queries - Replace migrate.ts with seed.ts for initial data - Update Dockerfile for Prisma lifecycle (copy generated client) - Update tsconfig.json with @/generated/* path alias - Remove pg and @types/pg dependencies - Add prisma.config.ts for Prisma 6 config - Update .gitignore for generated Prisma client --- .gitignore | 4 + Dockerfile | 22 +- package-lock.json | 1057 +++++++++++++++++++++++----- package.json | 10 +- prisma.config.ts | 11 + prisma/schema.prisma | 76 ++ src/app/api/kanban/route.ts | 65 +- src/app/api/projects/[id]/route.ts | 50 +- src/app/api/projects/route.ts | 57 +- src/app/api/tasks/[id]/route.ts | 182 ++--- src/app/api/tasks/route.ts | 148 ++-- src/lib/db.ts | 13 + src/server/db/migrate.ts | 124 ---- src/server/db/seed.ts | 63 ++ src/test/setup.ts | 8 - start.sh | 13 +- tsconfig.json | 5 +- 17 files changed, 1274 insertions(+), 634 deletions(-) create mode 100644 prisma.config.ts create mode 100644 prisma/schema.prisma create mode 100644 src/lib/db.ts delete mode 100644 src/server/db/migrate.ts create mode 100644 src/server/db/seed.ts delete mode 100644 src/test/setup.ts diff --git a/.gitignore b/.gitignore index 5ef6a52..ff9cac3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Prisma +!prisma/migrations/** +src/generated/ diff --git a/Dockerfile b/Dockerfile index 30da7a6..6c8c79f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,9 @@ RUN npm ci # Copy source code COPY . . +# Generate Prisma client +RUN npx prisma generate + # Build the Next.js app RUN npm run build @@ -24,9 +27,8 @@ ENV NODE_ENV=production ENV HOSTNAME="0.0.0.0" ENV PORT=3000 -# Install postgres client, netcat, and tsx for migrations -RUN apk add --no-cache postgresql-client netcat-openbsd && \ - npm install -g tsx +# Install postgres client and netcat +RUN apk add --no-cache postgresql-client netcat-openbsd # Create a non-root user RUN addgroup --system --gid 1001 nodejs && \ @@ -34,14 +36,14 @@ RUN addgroup --system --gid 1001 nodejs && \ # Copy the built app from builder COPY --from=builder /app/public ./public -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone/ ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next COPY --from=builder /app/package.json ./ +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/prisma ./prisma +# Copy generated Prisma client (includes native query engine binary) +COPY --from=builder /app/src/generated ./src/generated -# Copy migration script (for initial setup) -COPY --chown=nextjs:nodejs src/server/db/migrate.ts ./migrate.ts - -# Create startup script +# Copy startup script COPY start.sh /app/start.sh RUN chmod +x /app/start.sh @@ -49,4 +51,4 @@ USER nextjs EXPOSE 3000 -CMD ["/app/start.sh"] \ No newline at end of file +CMD ["/app/start.sh"] diff --git a/package-lock.json b/package-lock.json index a37e1bc..76bafb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,21 +8,22 @@ "name": "vixtix", "version": "0.1.0", "dependencies": { + "@prisma/client": "^6.19.3", "date-fns": "^4.1.0", "next": "16.2.4", - "pg": "^8.20.0", + "prisma": "^6.19.3", "react": "19.2.4", "react-dom": "19.2.4" }, "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", - "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.4", "tailwindcss": "^4", + "tsx": "^4.19.0", "typescript": "^5" } }, @@ -312,6 +313,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -1243,6 +1686,85 @@ "node": ">=12.4.0" } }, + "node_modules/@prisma/client": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", + "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", + "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.21.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", + "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", + "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.3", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", + "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", + "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1250,6 +1772,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1572,18 +2100,6 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/pg": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", - "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -2531,6 +3047,34 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2628,6 +3172,30 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2661,6 +3229,21 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2786,6 +3369,15 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2822,6 +3414,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2845,6 +3449,18 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2860,6 +3476,16 @@ "node": ">= 0.4" } }, + "node_modules/effect": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", + "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.349", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz", @@ -2874,6 +3500,15 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/enhanced-resolve": { "version": "5.21.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", @@ -3065,6 +3700,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3494,6 +4171,34 @@ "node": ">=0.10.0" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3635,6 +4340,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3766,6 +4486,23 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4443,7 +5180,6 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -5098,6 +5834,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.38", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", @@ -5105,6 +5847,29 @@ "dev": true, "license": "MIT" }, + "node_modules/nypm": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.6.tgz", + "integrity": "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==", + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.1.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5228,6 +5993,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5336,95 +6107,18 @@ "dev": true, "license": "MIT" }, - "node_modules/pg": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", - "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", - "license": "MIT", - "dependencies": { - "pg-connection-string": "^2.12.0", - "pg-pool": "^3.13.0", - "pg-protocol": "^1.13.0", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.3.0" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", - "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", - "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "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.13.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", - "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", - "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", "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/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5444,6 +6138,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5483,45 +6188,6 @@ "node": "^10 || ^12 || >=14" } }, - "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/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5532,6 +6198,31 @@ "node": ">= 0.8.0" } }, + "node_modules/prisma": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", + "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.3", + "@prisma/engines": "6.19.3" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5554,6 +6245,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5575,6 +6282,16 @@ ], "license": "MIT" }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -5603,6 +6320,19 @@ "dev": true, "license": "MIT" }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6012,15 +6742,6 @@ "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/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -6248,6 +6969,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -6354,6 +7084,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6449,7 +7199,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6700,15 +7450,6 @@ "node": ">=0.10.0" } }, - "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" - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index aaf47d6..fe59314 100644 --- a/package.json +++ b/package.json @@ -6,24 +6,28 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "db:generate": "prisma generate", + "db:migrate": "prisma migrate deploy", + "db:seed": "tsx src/server/db/seed.ts" }, "dependencies": { + "@prisma/client": "^6.19.3", "date-fns": "^4.1.0", "next": "16.2.4", - "pg": "^8.20.0", + "prisma": "^6.19.3", "react": "19.2.4", "react-dom": "19.2.4" }, "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", - "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.4", "tailwindcss": "^4", + "tsx": "^4.19.0", "typescript": "^5" } } diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..aae8bcd --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: process.env["DATABASE_URL"] || "postgresql://vixtix:vixtix_secret@localhost:5433/vixtix", + }, +}); diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..45276e1 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,76 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Project { + id String @id @default(uuid()) @db.Uuid + name String + description String @default("") + color String @default("#3b82f6") + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + tasks Task[] + + @@index([sortOrder]) +} + +model Task { + id String @id @default(uuid()) @db.Uuid + title String + description String @default("") + completed Boolean @default(false) + priority Priority @default(medium) + dueDate DateTime? @map("due_date") + status Status @default(todo) + parentTaskId String? @map("parent_task_id") @db.Uuid + recurrenceRule Recurrence @default(none) @map("recurrence_rule") + recurrenceInterval Int @default(1) @map("recurrence_interval") + nextOccurrence DateTime? @map("next_occurrence") + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + parentTask Task? @relation("TaskChildren", fields: [parentTaskId], references: [id], onDelete: Cascade) + children Task[] @relation("TaskChildren") + project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade) + projectId String? @map("project_id") @db.Uuid + + @@index([projectId]) + @@index([parentTaskId]) + @@index([completed]) + @@index([status]) + @@index([dueDate]) + @@index([recurrenceRule]) +} + +enum Priority { + low + medium + high + urgent +} + +enum Status { + todo + in_progress + done +} + +enum Recurrence { + none + daily + weekly + biweekly + monthly + yearly +} diff --git a/src/app/api/kanban/route.ts b/src/app/api/kanban/route.ts index 628f532..24a17ab 100644 --- a/src/app/api/kanban/route.ts +++ b/src/app/api/kanban/route.ts @@ -1,59 +1,34 @@ import { NextRequest, NextResponse } from 'next/server'; -import { Pool } from 'pg'; - -const pool = new Pool({ - connectionString: process.env.DATABASE_URL || 'postgresql://vixtix:vixtix_secret@localhost:5433/vixtix', -}); - -function serializeRow(row: Record): Record { - const toCamel = (str: string) => str.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); - const result: Record = {}; - for (const [key, value] of Object.entries(row)) { - const camelKey = toCamel(key); - if (value instanceof Date && !isNaN(value.getTime())) { - if (value.getUTCHours() === 0 && value.getUTCMinutes() === 0 && value.getUTCSeconds() === 0 && value.getUTCMilliseconds() === 0) { - const year = value.getUTCFullYear(); - const month = String(value.getUTCMonth() + 1).padStart(2, '0'); - const day = String(value.getUTCDate()).padStart(2, '0'); - result[camelKey] = `${year}-${month}-${day}`; - } else { - result[camelKey] = value.toISOString(); - } - } else { - result[camelKey] = value; - } - } - return result; -} +import prisma from '@/lib/db'; export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const projectId = searchParams.get('project'); try { - let whereClause = 'parent_task_id IS NULL'; - let params: any[] = []; - let paramIndex = 1; + const where: Record = { parentTaskId: null }; + if (projectId) where.projectId = projectId; - if (projectId) { - whereClause += ` AND project_id = $${paramIndex++}`; - params.push(projectId); - } + const tasks = await prisma.task.findMany({ + where, + include: { + project: { select: { color: true, name: true } }, + }, + orderBy: { sortOrder: 'asc' }, + }); - const result = await pool.query( - `SELECT t.*, p.color as project_color, p.name as project_name - FROM tasks t - LEFT JOIN projects p ON t.project_id = p.id - WHERE ${whereClause} - ORDER BY t.sort_order`, - params - ); + const rows = tasks.map((t) => ({ + ...t, + dueDate: t.dueDate ? t.dueDate.toISOString().split('T')[0] : null, + nextOccurrence: t.nextOccurrence ? t.nextOccurrence.toISOString().split('T')[0] : null, + projectColor: t.project?.color, + projectName: t.project?.name, + })); - const rows = result.rows.map(serializeRow); const grouped = { - todo: rows.filter((t: any) => t.status === 'todo'), - in_progress: rows.filter((t: any) => t.status === 'in_progress'), - done: rows.filter((t: any) => t.status === 'done'), + todo: rows.filter((t) => t.status === 'todo'), + in_progress: rows.filter((t) => t.status === 'in_progress'), + done: rows.filter((t) => t.status === 'done'), }; return NextResponse.json(grouped); diff --git a/src/app/api/projects/[id]/route.ts b/src/app/api/projects/[id]/route.ts index 97e2390..02fe4bf 100644 --- a/src/app/api/projects/[id]/route.ts +++ b/src/app/api/projects/[id]/route.ts @@ -1,23 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { Pool } from 'pg'; - -const pool = new Pool({ - connectionString: process.env.DATABASE_URL || 'postgresql://vixtix:vixtix_secret@localhost:5433/vixtix', -}); - -function serializeRow(row: Record): Record { - const toCamel = (str: string) => str.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); - const result: Record = {}; - for (const [key, value] of Object.entries(row)) { - const camelKey = toCamel(key); - if (value instanceof Date) { - result[camelKey] = value.toISOString(); - } else { - result[camelKey] = value; - } - } - return result; -} +import prisma from '@/lib/db'; export async function GET( request: NextRequest, @@ -26,13 +8,13 @@ export async function GET( const { id } = await params; try { - const result = await pool.query('SELECT * FROM projects WHERE id = $1', [id]); + const project = await prisma.project.findUnique({ where: { id } }); - if (result.rows.length === 0) { + if (!project) { return NextResponse.json({ error: 'Project not found' }, { status: 404 }); } - return NextResponse.json(serializeRow(result.rows[0])); + return NextResponse.json(project); } catch (error) { console.error('Error fetching project:', error); return NextResponse.json({ error: 'Failed to fetch project' }, { status: 500 }); @@ -59,16 +41,17 @@ export async function PUT( } try { - const result = await pool.query( - `UPDATE projects SET name = $1, description = $2, color = $3, sort_order = $4 WHERE id = $5 RETURNING *`, - [name.trim(), description?.trim() || '', color, sortOrder, id] - ); + const project = await prisma.project.update({ + where: { id }, + data: { + name: name.trim(), + description: description?.trim() || '', + color: color, + sortOrder: sortOrder, + }, + }); - if (result.rows.length === 0) { - return NextResponse.json({ error: 'Project not found' }, { status: 404 }); - } - - return NextResponse.json(serializeRow(result.rows[0])); + return NextResponse.json(project); } catch (error) { console.error('Error updating project:', error); return NextResponse.json({ error: 'Failed to update project' }, { status: 500 }); @@ -82,10 +65,7 @@ export async function DELETE( const { id } = await params; try { - // Delete all tasks in the project first - await pool.query('DELETE FROM tasks WHERE project_id = $1', [id]); - // Then delete the project - await pool.query('DELETE FROM projects WHERE id = $1', [id]); + await prisma.project.delete({ where: { id } }); return NextResponse.json({ success: true }); } catch (error) { diff --git a/src/app/api/projects/route.ts b/src/app/api/projects/route.ts index 78853ed..694a05b 100644 --- a/src/app/api/projects/route.ts +++ b/src/app/api/projects/route.ts @@ -1,35 +1,12 @@ import { NextRequest, NextResponse } from 'next/server'; -import { Pool } from 'pg'; +import prisma from '@/lib/db'; -const pool = new Pool({ - connectionString: process.env.DATABASE_URL || 'postgresql://vixtix:vixtix_secret@localhost:5433/vixtix', -}); - -function serializeRow(row: Record): Record { - const toCamel = (str: string) => str.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); - const result: Record = {}; - for (const [key, value] of Object.entries(row)) { - const camelKey = toCamel(key); - if (value instanceof Date && !isNaN(value.getTime())) { - if (value.getUTCHours() === 0 && value.getUTCMinutes() === 0 && value.getUTCSeconds() === 0 && value.getUTCMilliseconds() === 0) { - const year = value.getUTCFullYear(); - const month = String(value.getUTCMonth() + 1).padStart(2, '0'); - const day = String(value.getUTCDate()).padStart(2, '0'); - result[camelKey] = `${year}-${month}-${day}`; - } else { - result[camelKey] = value.toISOString(); - } - } else { - result[camelKey] = value; - } - } - return result; -} - -export async function GET(request: NextRequest) { +export async function GET() { try { - const result = await pool.query('SELECT * FROM projects ORDER BY sort_order'); - return NextResponse.json(result.rows.map(serializeRow)); + const projects = await prisma.project.findMany({ + orderBy: { sortOrder: 'asc' }, + }); + return NextResponse.json(projects); } catch (error) { console.error('Error fetching projects:', error); return NextResponse.json({ error: 'Failed to fetch projects' }, { status: 500 }); @@ -50,14 +27,22 @@ export async function POST(request: NextRequest) { } try { - const maxSort = await pool.query('SELECT COALESCE(MAX(sort_order), -1) as max FROM projects'); - const result = await pool.query( - `INSERT INTO projects (name, description, color, sort_order) VALUES ($1, $2, $3, $4) RETURNING *`, - [name.trim(), description, color, (maxSort.rows[0].max ?? -1) + 1] - ); - return NextResponse.json(serializeRow(result.rows[0]), { status: 201 }); + const maxSort = await prisma.project.aggregate({ + _max: { sortOrder: true }, + }); + + const project = await prisma.project.create({ + data: { + name: name.trim(), + description: description, + color: color, + sortOrder: (maxSort._max.sortOrder ?? -1) + 1, + }, + }); + + return NextResponse.json(project, { status: 201 }); } catch (error) { console.error('Error creating project:', error); return NextResponse.json({ error: 'Failed to create project' }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/src/app/api/tasks/[id]/route.ts b/src/app/api/tasks/[id]/route.ts index b3cc732..4ecac38 100644 --- a/src/app/api/tasks/[id]/route.ts +++ b/src/app/api/tasks/[id]/route.ts @@ -1,35 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; -import { Pool } from 'pg'; +import prisma from '@/lib/db'; import { addDays, addWeeks, addMonths, addYears } from 'date-fns'; -const pool = new Pool({ - connectionString: process.env.DATABASE_URL || 'postgresql://vixtix:vixtix_secret@localhost:5433/vixtix', -}); - -function toCamel(str: string): string { - return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); -} - -function serializeRow(row: Record): Record { - const result: Record = {}; - for (const [key, value] of Object.entries(row)) { - const camelKey = toCamel(key); - if (value instanceof Date && !isNaN(value.getTime())) { - if (value.getUTCHours() === 0 && value.getUTCMinutes() === 0 && value.getUTCSeconds() === 0 && value.getUTCMilliseconds() === 0) { - const year = value.getUTCFullYear(); - const month = String(value.getUTCMonth() + 1).padStart(2, '0'); - const day = String(value.getUTCDate()).padStart(2, '0'); - result[camelKey] = `${year}-${month}-${day}`; - } else { - result[camelKey] = value.toISOString(); - } - } else { - result[camelKey] = value; - } - } - return result; -} - export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } @@ -37,23 +9,26 @@ export async function GET( const { id } = await params; try { - const taskResult = await pool.query( - 'SELECT * FROM tasks WHERE id = $1', - [id] - ); + const task = await prisma.task.findUnique({ + where: { id }, + include: { + children: { orderBy: { sortOrder: 'asc' } }, + }, + }); - if (taskResult.rows.length === 0) { + if (!task) { return NextResponse.json({ error: 'Task not found' }, { status: 404 }); } - const subtaskResult = await pool.query( - 'SELECT * FROM tasks WHERE parent_task_id = $1 ORDER BY sort_order', - [id] - ); - return NextResponse.json({ - ...serializeRow(taskResult.rows[0]), - subtasks: subtaskResult.rows.map(serializeRow), + ...task, + dueDate: task.dueDate ? task.dueDate.toISOString().split('T')[0] : null, + nextOccurrence: task.nextOccurrence ? task.nextOccurrence.toISOString().split('T')[0] : null, + subtasks: task.children.map((c) => ({ + ...c, + dueDate: c.dueDate ? c.dueDate.toISOString().split('T')[0] : null, + nextOccurrence: c.nextOccurrence ? c.nextOccurrence.toISOString().split('T')[0] : null, + })), }); } catch (error) { console.error('Error fetching task:', error); @@ -74,46 +49,33 @@ export async function PUT( return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } - const { - title, - description, - projectId, - priority, - dueDate, - status, - recurrenceRule, - recurrenceInterval, - nextOccurrence, - } = body; + const { title, description, projectId, priority, dueDate, status, recurrenceRule, recurrenceInterval, nextOccurrence } = body; if (!title?.trim()) { return NextResponse.json({ error: 'Title is required' }, { status: 400 }); } try { - const result = await pool.query( - `UPDATE tasks SET title = $1, description = $2, project_id = $3, priority = $4, due_date = $5, status = $6, recurrence_rule = $7, recurrence_interval = $8, next_occurrence = $9, updated_at = $10 - WHERE id = $11 RETURNING *`, - [ - title.trim(), - description?.trim() || '', - projectId, - priority, - dueDate || null, - status, - recurrenceRule, - recurrenceInterval, - nextOccurrence || null, - new Date(), - id, - ] - ); + const task = await prisma.task.update({ + where: { id }, + data: { + title: title.trim(), + description: description?.trim() || '', + projectId: projectId || undefined, + priority: priority, + dueDate: dueDate ? new Date(dueDate) : undefined, + status: status, + recurrenceRule: recurrenceRule, + recurrenceInterval: recurrenceInterval, + nextOccurrence: nextOccurrence ? new Date(nextOccurrence) : undefined, + }, + }); - if (result.rows.length === 0) { - return NextResponse.json({ error: 'Task not found' }, { status: 404 }); - } - - return NextResponse.json(serializeRow(result.rows[0])); + return NextResponse.json({ + ...task, + dueDate: task.dueDate ? task.dueDate.toISOString().split('T')[0] : null, + nextOccurrence: task.nextOccurrence ? task.nextOccurrence.toISOString().split('T')[0] : null, + }); } catch (error) { console.error('Error updating task:', error); return NextResponse.json({ error: 'Failed to update task' }, { status: 500 }); @@ -127,8 +89,8 @@ export async function DELETE( const { id } = await params; try { - await pool.query('DELETE FROM tasks WHERE parent_task_id = $1', [id]); - await pool.query('DELETE FROM tasks WHERE id = $1', [id]); + await prisma.task.deleteMany({ where: { parentTaskId: id } }); + await prisma.task.delete({ where: { id } }); return NextResponse.json({ success: true }); } catch (error) { @@ -145,48 +107,48 @@ export async function PATCH( const { action } = await request.json(); try { - const taskResult = await pool.query('SELECT * FROM tasks WHERE id = $1', [id]); + const task = await prisma.task.findUnique({ where: { id } }); - if (taskResult.rows.length === 0) { + if (!task) { return NextResponse.json({ error: 'Task not found' }, { status: 404 }); } - const task = taskResult.rows[0]; - if (action === 'toggle-done') { // If completing a recurring task, create the next occurrence - if (!task.completed && task.recurrence_rule !== 'none' && task.due_date) { - const currentDate = new Date(task.due_date); - const nextDate = getNextOccurrence(currentDate, task.recurrence_rule, task.recurrence_interval); - // Convert to "YYYY-MM-DD" string for DATE column - const nextDateStr = nextDate.toISOString().split('T')[0]; - - await pool.query( - `INSERT INTO tasks (title, description, project_id, priority, due_date, status, completed, parent_task_id, recurrence_rule, recurrence_interval, sort_order, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, 'todo', FALSE, $6, $7, $8, $9, $10, $11)`, - [ - task.title, - task.description, - task.project_id, - task.priority, - nextDateStr, - task.id, - task.recurrence_rule, - task.recurrence_interval, - (task.sort_order || 0) + 1, - new Date(), - new Date(), - ] - ); + if (!task.completed && task.recurrenceRule !== 'none' && task.dueDate) { + const currentDate = new Date(task.dueDate); + const nextDate = getNextOccurrence(currentDate, task.recurrenceRule, task.recurrenceInterval); + + await prisma.task.create({ + data: { + title: task.title, + description: task.description, + projectId: task.projectId || undefined, + priority: task.priority, + dueDate: nextDate, + status: 'todo', + completed: false, + parentTaskId: task.id, + recurrenceRule: task.recurrenceRule, + recurrenceInterval: task.recurrenceInterval, + sortOrder: (task.sortOrder || 0) + 1, + }, + }); } - await pool.query( - `UPDATE tasks SET completed = NOT completed, status = CASE WHEN NOT completed THEN 'done' ELSE status END, updated_at = $1 WHERE id = $2`, - [new Date(), id] - ); + const updated = await prisma.task.update({ + where: { id }, + data: { + completed: { set: !task.completed }, + status: task.completed ? 'todo' : 'done', + }, + }); - const updatedResult = await pool.query('SELECT * FROM tasks WHERE id = $1', [id]); - return NextResponse.json(serializeRow(updatedResult.rows[0])); + return NextResponse.json({ + ...updated, + dueDate: updated.dueDate ? updated.dueDate.toISOString().split('T')[0] : null, + nextOccurrence: updated.nextOccurrence ? updated.nextOccurrence.toISOString().split('T')[0] : null, + }); } return NextResponse.json({ error: 'Unknown action' }, { status: 400 }); @@ -211,4 +173,4 @@ function getNextOccurrence(fromDate: Date, rule: string, interval: number): Date default: return fromDate; } -} \ No newline at end of file +} diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index 05a5bce..1d8dc67 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -1,42 +1,9 @@ import { NextRequest, NextResponse } from 'next/server'; -import { Pool } from 'pg'; - -const pool = new Pool({ - connectionString: process.env.DATABASE_URL || 'postgresql://vixtix:vixtix_secret@localhost:5433/vixtix', -}); - -function toCamel(str: string): string { - return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); -} - -function serializeRow(row: Record): Record { - const result: Record = {}; - for (const [key, value] of Object.entries(row)) { - const camelKey = toCamel(key); - if (value instanceof Date && !isNaN(value.getTime())) { - // pg driver converts both DATE and TIMESTAMPTZ to Date objects - // DATE columns are at midnight UTC (00:00:00.000) - // TIMESTAMPTZ columns have actual time components - if (value.getUTCHours() === 0 && value.getUTCMinutes() === 0 && value.getUTCSeconds() === 0 && value.getUTCMilliseconds() === 0) { - // DATE column - return "YYYY-MM-DD" - const year = value.getUTCFullYear(); - const month = String(value.getUTCMonth() + 1).padStart(2, '0'); - const day = String(value.getUTCDate()).padStart(2, '0'); - result[camelKey] = `${year}-${month}-${day}`; - } else { - // TIMESTAMPTZ column - return ISO string - result[camelKey] = value.toISOString(); - } - } else { - result[camelKey] = value; - } - } - return result; -} +import prisma from '@/lib/db'; export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); - + const project = searchParams.get('project'); const search = searchParams.get('search'); const status = searchParams.get('status'); @@ -47,56 +14,30 @@ export async function GET(request: NextRequest) { const sortBy = searchParams.get('sort_by') || 'due_date'; try { - // Build dynamic query - let query = 'SELECT * FROM tasks WHERE 1=1'; - const params: any[] = []; - let paramIndex = 1; + const where: Record = {}; - if (project) { - query += ` AND project_id = $${paramIndex}`; - params.push(project); - paramIndex++; - } - if (search) { - query += ` AND title ILIKE $${paramIndex}`; - params.push(`%${search}%`); - paramIndex++; - } - if (status) { - query += ` AND status = $${paramIndex}`; - params.push(status); - paramIndex++; - } - if (priority) { - query += ` AND priority = $${paramIndex}`; - params.push(priority); - paramIndex++; - } - if (dueBefore) { - query += ` AND due_date <= $${paramIndex}`; - params.push(dueBefore); - paramIndex++; - } - if (dueAfter) { - query += ` AND due_date >= $${paramIndex}`; - params.push(dueAfter); - paramIndex++; - } - if (completed !== null) { - query += ` AND completed = $${paramIndex}`; - params.push(completed === 'true'); - paramIndex++; - } + if (project) where.projectId = project; + if (search) where.title = { contains: search, mode: 'insensitive' as const }; + if (status) where.status = status; + if (priority) where.priority = priority; + if (dueBefore) where.dueDate = { lte: new Date(dueBefore) }; + if (dueAfter) where.dueDate = { gte: new Date(dueAfter) }; + if (completed !== null) where.completed = completed === 'true'; - // Sort order - const sortColumn = sortBy === 'priority' ? 'priority' : 'due_date'; - const sortOrder = sortBy === 'priority' ? 'CASE priority WHEN \'urgent\' THEN 1 WHEN \'high\' THEN 2 WHEN \'medium\' THEN 3 WHEN \'low\' THEN 4 END' : 'due_date'; - query += ` ORDER BY ${sortColumn === 'due_date' ? 'due_date NULLS LAST' : sortOrder}`; - query += ', title'; + // Sort order - Prisma 6 requires orderBy as an array of sort objects + const orderBy: Array> = sortBy === 'priority' + ? [{ priority: 'asc' }, { title: 'asc' }] + : [{ dueDate: { sort: 'asc', nulls: 'last' } }, { title: 'asc' }]; - const result = await pool.query(query, params); + const tasks = await prisma.task.findMany({ where, orderBy }); - return NextResponse.json(result.rows.map(serializeRow)); + return NextResponse.json( + tasks.map((t) => ({ + ...t, + dueDate: t.dueDate ? t.dueDate.toISOString().split('T')[0] : null, + nextOccurrence: t.nextOccurrence ? t.nextOccurrence.toISOString().split('T')[0] : null, + })) + ); } catch (error) { console.error('Error fetching tasks:', error); return NextResponse.json({ error: 'Failed to fetch tasks' }, { status: 500 }); @@ -105,7 +46,6 @@ export async function GET(request: NextRequest) { export async function POST(request: NextRequest) { let body; - try { body = await request.json(); } catch { @@ -119,26 +59,34 @@ export async function POST(request: NextRequest) { } try { - const result = await pool.query( - `INSERT INTO tasks (title, description, project_id, priority, due_date, status, recurrence_rule, recurrence_interval, parent_task_id, sort_order) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, (SELECT COALESCE(MAX(sort_order), 0) + 1 FROM tasks)) - RETURNING *`, - [ - title.trim(), - description?.trim() || '', - projectId || null, - priority || 'medium', - dueDate || null, - status || 'todo', - recurrenceRule || 'none', - recurrenceInterval || 1, - parentTaskId || null, - ] - ); + // Get max sort_order for the project (or overall if no project) + const maxSort = await prisma.task.aggregate({ + where: projectId ? { projectId } : undefined, + _max: { sortOrder: true }, + }); - return NextResponse.json(serializeRow(result.rows[0])); + const task = await prisma.task.create({ + data: { + title: title.trim(), + description: description?.trim() || '', + projectId: projectId || undefined, + priority: priority || 'medium', + dueDate: dueDate ? new Date(dueDate) : undefined, + status: status || 'todo', + recurrenceRule: recurrenceRule || 'none', + recurrenceInterval: recurrenceInterval || 1, + parentTaskId: parentTaskId || undefined, + sortOrder: (maxSort._max.sortOrder ?? 0) + 1, + }, + }); + + return NextResponse.json({ + ...task, + dueDate: task.dueDate ? task.dueDate.toISOString().split('T')[0] : null, + nextOccurrence: task.nextOccurrence ? task.nextOccurrence.toISOString().split('T')[0] : null, + }); } catch (error) { console.error('Error creating task:', error); return NextResponse.json({ error: 'Failed to create task' }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/src/lib/db.ts b/src/lib/db.ts new file mode 100644 index 0000000..9a9bc01 --- /dev/null +++ b/src/lib/db.ts @@ -0,0 +1,13 @@ +import { PrismaClient } from '@/generated/prisma/client'; + +const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; + +export const prisma = + globalForPrisma.prisma || + new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], + }); + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; + +export default prisma; diff --git a/src/server/db/migrate.ts b/src/server/db/migrate.ts deleted file mode 100644 index 7cb8283..0000000 --- a/src/server/db/migrate.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Pool } from 'pg'; - -const connectionString = process.env.DATABASE_URL || 'postgresql://vixtix:vixtix_secret@localhost:5433/vixtix'; -const pool = new Pool({ connectionString }); - -async function migrate() { - console.log('Running migrations...'); - - const client = await pool.connect(); - try { - await client.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'); - - // Create enums - use DO block to handle duplicates - await client.query(` - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'priority') THEN - CREATE TYPE priority AS ENUM ('low', 'medium', 'high', 'urgent'); - END IF; - END $$; - `); - await client.query(` - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'status') THEN - CREATE TYPE status AS ENUM ('todo', 'in_progress', 'done'); - END IF; - END $$; - `); - await client.query(` - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'recurrence') THEN - CREATE TYPE recurrence AS ENUM ('daily', 'weekly', 'biweekly', 'monthly', 'yearly', 'none'); - END IF; - END $$; - `); - - await client.query(` - CREATE TABLE IF NOT EXISTS projects ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, description TEXT DEFAULT '', color TEXT DEFAULT '#3b82f6', - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL - ); - `); - - await client.query(` - CREATE TABLE IF NOT EXISTS tasks ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - project_id UUID REFERENCES projects(id), - title TEXT NOT NULL, description TEXT DEFAULT '', - completed BOOLEAN DEFAULT FALSE NOT NULL, - priority priority DEFAULT 'medium' NOT NULL, - due_date DATE, - status status DEFAULT 'todo' NOT NULL, - parent_task_id UUID REFERENCES tasks(id), - recurrence_rule recurrence DEFAULT 'none' NOT NULL, - recurrence_interval INTEGER DEFAULT 1 NOT NULL, - next_occurrence DATE, - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL - ); - `); - - await client.query(` - CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id); - CREATE INDEX IF NOT EXISTS idx_tasks_parent_task_id ON tasks(parent_task_id); - CREATE INDEX IF NOT EXISTS idx_tasks_completed ON tasks(completed); - CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); - CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date); - CREATE INDEX IF NOT EXISTS idx_tasks_recurrence ON tasks(recurrence_rule); - `); - - const count = await client.query('SELECT COUNT(*) FROM projects'); - if (parseInt(count.rows[0].count) === 0) { - console.log('Seeding initial data...'); - await client.query(` - INSERT INTO projects (name, description, color, sort_order) VALUES - ('Personal', 'Personal tasks and goals', '#3b82f6', 1), - ('Work', 'Work-related tasks', '#10b981', 2), - ('Health', 'Health and fitness', '#f59e0b', 3), - ('Finance', 'Financial tasks and tracking', '#8b5cf6', 4); - `); - - // Use DATE arithmetic instead of INTERVAL - await client.query(` - INSERT INTO tasks (project_id, title, description, priority, status, due_date, sort_order) - SELECT p.id, t.title, t.description, t.priority::priority, t.status::status, t.due_date, t.sort_order - FROM ( - VALUES - ('Personal', 'Set up daily routine', 'Morning meditation, exercise, and planning', 'high', 'todo', CURRENT_DATE + 1, 1), - ('Personal', 'Read 30 minutes', 'Read a book or articles', 'medium', 'todo', CURRENT_DATE + 2, 2), - ('Personal', 'Clean apartment', 'Deep clean kitchen and bathrooms', 'medium', 'in_progress', CURRENT_DATE + 3, 3), - ('Work', 'Review sprint backlog', 'Prioritize tasks for next sprint', 'high', 'todo', CURRENT_DATE + 1, 1), - ('Work', 'Update documentation', 'Add API docs for new endpoints', 'medium', 'in_progress', CURRENT_DATE + 5, 2), - ('Work', 'Code review', 'Review pull requests from team', 'low', 'done', CURRENT_DATE - 1, 3), - ('Health', 'Gym workout', 'Upper body strength training', 'high', 'todo', CURRENT_DATE + 1, 1), - ('Health', 'Meal prep', 'Prepare healthy meals for the week', 'medium', 'todo', CURRENT_DATE + 2, 2), - ('Health', 'Track water intake', 'Drink at least 8 glasses of water', 'low', 'done', CURRENT_DATE - 1, 3), - ('Finance', 'Review monthly budget', 'Check spending and adjust categories', 'high', 'todo', CURRENT_DATE + 3, 1), - ('Finance', 'Pay bills', 'Electricity, internet, phone', 'urgent', 'todo', CURRENT_DATE + 1, 2), - ('Finance', 'Investment review', 'Check portfolio performance', 'medium', 'todo', CURRENT_DATE + 7, 3) - ) AS t(name, title, description, priority, status, due_date, sort_order) - JOIN projects p ON p.name = t.name; - `); - console.log('Seed data inserted successfully!'); - } else { - console.log('Database already seeded.'); - } - - console.log('Migrations completed successfully!'); - } catch (error) { - console.error('Migration failed:', error); - process.exit(1); - } finally { - client.release(); - await pool.end(); - } -} - -migrate(); diff --git a/src/server/db/seed.ts b/src/server/db/seed.ts new file mode 100644 index 0000000..1356fcd --- /dev/null +++ b/src/server/db/seed.ts @@ -0,0 +1,63 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], +}); + +async function seed() { + console.log('Seeding database...'); + + const count = await prisma.project.count(); + if (count > 0) { + console.log('Database already seeded.'); + return; + } + + const projects = await prisma.project.createManyAndReturn({ + data: [ + { name: 'Personal', description: 'Personal tasks and goals', color: '#3b82f6', sortOrder: 1 }, + { name: 'Work', description: 'Work-related tasks', color: '#10b981', sortOrder: 2 }, + { name: 'Health', description: 'Health and fitness', color: '#f59e0b', sortOrder: 3 }, + { name: 'Finance', description: 'Financial tasks and tracking', color: '#8b5cf6', sortOrder: 4 }, + ], + }) as Array<{ id: string; name: string }>; + + // Map project names to IDs + const projectMap = new Map(projects.map(p => [p.name, p.id])); + + const today = new Date(); + const tasks: Array<{ + projectId: string; + title: string; + description: string; + priority: 'low' | 'medium' | 'high' | 'urgent'; + status: 'todo' | 'in_progress' | 'done'; + dueDate: Date; + sortOrder: number; + }> = [ + { projectId: projectMap.get('Personal')!, title: 'Set up daily routine', description: 'Morning meditation, exercise, and planning', priority: 'high', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1), sortOrder: 1 }, + { projectId: projectMap.get('Personal')!, title: 'Read 30 minutes', description: 'Read a book or articles', priority: 'medium', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2), sortOrder: 2 }, + { projectId: projectMap.get('Personal')!, title: 'Clean apartment', description: 'Deep clean kitchen and bathrooms', priority: 'medium', status: 'in_progress', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3), sortOrder: 3 }, + { projectId: projectMap.get('Work')!, title: 'Review sprint backlog', description: 'Prioritize tasks for next sprint', priority: 'high', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1), sortOrder: 1 }, + { projectId: projectMap.get('Work')!, title: 'Update documentation', description: 'Add API docs for new endpoints', priority: 'medium', status: 'in_progress', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 5), sortOrder: 2 }, + { projectId: projectMap.get('Work')!, title: 'Code review', description: 'Review pull requests from team', priority: 'low', status: 'done', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1), sortOrder: 3 }, + { projectId: projectMap.get('Health')!, title: 'Gym workout', description: 'Upper body strength training', priority: 'high', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1), sortOrder: 1 }, + { projectId: projectMap.get('Health')!, title: 'Meal prep', description: 'Prepare healthy meals for the week', priority: 'medium', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2), sortOrder: 2 }, + { projectId: projectMap.get('Health')!, title: 'Track water intake', description: 'Drink at least 8 glasses of water', priority: 'low', status: 'done', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1), sortOrder: 3 }, + { projectId: projectMap.get('Finance')!, title: 'Review monthly budget', description: 'Check spending and adjust categories', priority: 'high', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3), sortOrder: 1 }, + { projectId: projectMap.get('Finance')!, title: 'Pay bills', description: 'Electricity, internet, phone', priority: 'urgent', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1), sortOrder: 2 }, + { projectId: projectMap.get('Finance')!, title: 'Investment review', description: 'Check portfolio performance', priority: 'medium', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7), sortOrder: 3 }, + ]; + + await prisma.task.createMany({ data: tasks }); + console.log('Seed data inserted successfully!'); +} + +seed() + .catch((e) => { + console.error('Seed failed:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/test/setup.ts b/src/test/setup.ts deleted file mode 100644 index 19c8d7f..0000000 --- a/src/test/setup.ts +++ /dev/null @@ -1,8 +0,0 @@ -import '@testing-library/jest-dom/vitest'; -import { cleanup } from '@testing-library/react'; -import { afterEach } from 'vitest'; - -// Auto cleanup after each test -afterEach(() => { - cleanup(); -}); \ No newline at end of file diff --git a/start.sh b/start.sh index a4d467d..5648dad 100644 --- a/start.sh +++ b/start.sh @@ -10,8 +10,15 @@ for i in $(seq 1 60); do sleep 1 done -echo "Running migrations..." -tsx ./migrate.ts +echo "Running Prisma migrations..." +# Check if migrations have already been applied +if [ -d "prisma/migrations" ] && [ "$(ls -A prisma/migrations 2>/dev/null)" ]; then + echo "Applying existing migrations..." + npx prisma migrate deploy +else + echo "Pushing schema to database (first run)..." + npx prisma db push --accept-data-loss +fi echo "Starting VixTix..." -exec node server.js +exec npx next start -p 3000 diff --git a/tsconfig.json b/tsconfig.json index cf9c65d..0521ced 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,8 @@ } ], "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@/generated/*": ["./src/generated/*"] } }, "include": [ @@ -30,5 +31,5 @@ ".next/dev/types/**/*.ts", "**/*.mts" ], - "exclude": ["node_modules"] + "exclude": ["node_modules", "vitest.config.ts"] }