diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..4346892 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/lib/db/schema.ts", + out: "./drizzle", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/package.json b/package.json index 82cdaf1..cd5fd2b 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,11 @@ "version": "0.1.0", "private": true, "scripts": { - "generate": "prisma generate", - "postinstall": "prisma generate", + "db:generate": "drizzle-kit generate", + "db:push": "drizzle-kit push", "dev": "next dev --turbopack", "build": "next build", "build:test": "NODE_ENV=test next build", - "migrate:test": "dotenv -e .env.test -- prisma migrate dev", - "migrate:dev": "dotenv -e .env.local -- prisma migrate dev", "start": "next start", "lint": "next lint", "test:e2e": "playwright test" @@ -18,8 +16,6 @@ "@fastify/cors": "^10.0.2", "@hookform/resolvers": "^4.0.0", "@neondatabase/serverless": "^0.10.4", - "@prisma/adapter-neon": "^6.3.1", - "@prisma/client": "6.3.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", @@ -37,6 +33,8 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.13", + "drizzle-kit": "^0.31.8", + "drizzle-orm": "^0.45.1", "ethers": "^6.13.5", "fastify": "^5.2.1", "input-otp": "^1.4.2", @@ -45,7 +43,7 @@ "lucide-react": "^0.474.0", "motion": "^12.4.1", "next": "15.1.6", - "prisma": "^6.3.1", + "postgres": "^3.4.8", "react": "^19.0.0", "react-device-detect": "^2.2.3", "react-dom": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e557348..1746abb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,12 +17,6 @@ importers: '@neondatabase/serverless': specifier: ^0.10.4 version: 0.10.4 - '@prisma/adapter-neon': - specifier: ^6.3.1 - version: 6.3.1(@neondatabase/serverless@0.10.4) - '@prisma/client': - specifier: 6.3.1 - version: 6.3.1(prisma@6.3.1(typescript@5.7.3))(typescript@5.7.3) '@radix-ui/react-dialog': specifier: ^1.1.6 version: 1.1.6(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -74,6 +68,12 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 + drizzle-kit: + specifier: ^0.31.8 + version: 0.31.8 + drizzle-orm: + specifier: ^0.45.1 + version: 0.45.1(@neondatabase/serverless@0.10.4)(@prisma/client@6.3.1(prisma@6.3.1(typescript@5.7.3))(typescript@5.7.3))(@types/pg@8.11.6)(@upstash/redis@1.34.4)(postgres@3.4.8)(prisma@6.3.1(typescript@5.7.3)) ethers: specifier: ^6.13.5 version: 6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -98,9 +98,9 @@ importers: next: specifier: 15.1.6 version: 15.1.6(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - prisma: - specifier: ^6.3.1 - version: 6.3.1(typescript@5.7.3) + postgres: + specifier: ^3.4.8 + version: 3.4.8 react: specifier: ^19.0.0 version: 19.0.0 @@ -218,24 +218,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@1.9.4': resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@1.9.4': resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@1.9.4': resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@1.9.4': resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} @@ -255,9 +259,308 @@ packages: '@coinbase/wallet-sdk@4.0.3': resolution: {integrity: sha512-y/OGEjlvosikjfB+wk+4CVb9OxD1ob9cidEBLI5h8Hxaf/Qoob2XoVT1uvhtAzBx34KpGYSd+alKvh/GCRre4Q==} + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@emnapi/runtime@1.3.1': resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.4.1': resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -395,67 +698,79 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -568,24 +883,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@15.1.6': resolution: {integrity: sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@15.1.6': resolution: {integrity: sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@15.1.6': resolution: {integrity: sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@15.1.6': resolution: {integrity: sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==} @@ -646,11 +965,6 @@ packages: engines: {node: '>=18'} hasBin: true - '@prisma/adapter-neon@6.3.1': - resolution: {integrity: sha512-yDCeqQ0EYT/DBL6G2uh01IlVrCr9Jli6SXa1yvdl/Kq4O/Hjj1i+Oxg4FskHVStLvlo6bqNxEFtLEuFCLCXG1A==} - peerDependencies: - '@neondatabase/serverless': ^0.6.0 || ^0.7.0 || ^0.8.0 || ^0.9.0 || ^0.10.0 - '@prisma/client@6.3.1': resolution: {integrity: sha512-ARAJaPs+eBkemdky/XU3cvGRl+mIPHCN2lCXsl5Vlb0E2gV+R6IN7aCI8CisRGszEZondwIsW9Iz8EJkTdykyA==} engines: {node: '>=18.18'} @@ -666,9 +980,6 @@ packages: '@prisma/debug@6.3.1': resolution: {integrity: sha512-RrEBkd+HLZx+ydfmYT0jUj7wjLiS95wfTOSQ+8FQbvb6vHh5AeKfEPt/XUQ5+Buljj8hltEfOslEW57/wQIVeA==} - '@prisma/driver-adapter-utils@6.3.1': - resolution: {integrity: sha512-Z30oSnSFYlzU/Z96lfApqd42EzSXwxj7iPD34iGdZ+fecGlOO9WaBY2tlzCdt7sdhxIxNGEL8tD1o4EHX8tLNA==} - '@prisma/engines-version@6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0': resolution: {integrity: sha512-R/ZcMuaWZT2UBmgX3Ko6PAV3f8//ZzsjRIG1eKqp3f2rqEqVtCv+mtzuH2rBPUC9ujJ5kCb9wwpxeyCkLcHVyA==} @@ -1752,6 +2063,9 @@ packages: bs58@5.0.0: resolution: {integrity: sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -2007,6 +2321,102 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} + drizzle-kit@0.31.8: + resolution: {integrity: sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==} + hasBin: true + + drizzle-orm@0.45.1: + resolution: {integrity: sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2073,6 +2483,21 @@ packages: es6-promisify@5.0.0: resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -3163,6 +3588,10 @@ packages: postgres-range@1.1.4: resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + postgres@3.4.8: + resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} + engines: {node: '>=12'} + preact@10.25.4: resolution: {integrity: sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==} @@ -3494,6 +3923,13 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + split-on-first@1.1.0: resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} engines: {node: '>=6'} @@ -4031,11 +4467,167 @@ snapshots: preact: 10.25.4 sha.js: 2.4.11 + '@drizzle-team/brocli@0.10.2': {} + '@emnapi/runtime@1.3.1': dependencies: tslib: 2.8.1 optional: true + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.10.0 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + '@eslint-community/eslint-utils@4.4.1(eslint@9.19.0(jiti@1.21.7))': dependencies: eslint: 9.19.0(jiti@1.21.7) @@ -4404,24 +4996,17 @@ snapshots: dependencies: playwright: 1.50.1 - '@prisma/adapter-neon@6.3.1(@neondatabase/serverless@0.10.4)': - dependencies: - '@neondatabase/serverless': 0.10.4 - '@prisma/driver-adapter-utils': 6.3.1 - postgres-array: 3.0.2 - '@prisma/client@6.3.1(prisma@6.3.1(typescript@5.7.3))(typescript@5.7.3)': optionalDependencies: prisma: 6.3.1(typescript@5.7.3) typescript: 5.7.3 + optional: true - '@prisma/debug@6.3.1': {} - - '@prisma/driver-adapter-utils@6.3.1': - dependencies: - '@prisma/debug': 6.3.1 + '@prisma/debug@6.3.1': + optional: true - '@prisma/engines-version@6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0': {} + '@prisma/engines-version@6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0': + optional: true '@prisma/engines@6.3.1': dependencies: @@ -4429,16 +5014,19 @@ snapshots: '@prisma/engines-version': 6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0 '@prisma/fetch-engine': 6.3.1 '@prisma/get-platform': 6.3.1 + optional: true '@prisma/fetch-engine@6.3.1': dependencies: '@prisma/debug': 6.3.1 '@prisma/engines-version': 6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0 '@prisma/get-platform': 6.3.1 + optional: true '@prisma/get-platform@6.3.1': dependencies: '@prisma/debug': 6.3.1 + optional: true '@radix-ui/primitive@1.1.1': {} @@ -6455,6 +7043,8 @@ snapshots: base-x: 4.0.0 optional: true + buffer-from@1.1.2: {} + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -6683,6 +7273,24 @@ snapshots: dotenv@16.4.7: {} + drizzle-kit@0.31.8: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.45.1(@neondatabase/serverless@0.10.4)(@prisma/client@6.3.1(prisma@6.3.1(typescript@5.7.3))(typescript@5.7.3))(@types/pg@8.11.6)(@upstash/redis@1.34.4)(postgres@3.4.8)(prisma@6.3.1(typescript@5.7.3)): + optionalDependencies: + '@neondatabase/serverless': 0.10.4 + '@prisma/client': 6.3.1(prisma@6.3.1(typescript@5.7.3))(typescript@5.7.3) + '@types/pg': 8.11.6 + '@upstash/redis': 1.34.4 + postgres: 3.4.8 + prisma: 6.3.1(typescript@5.7.3) + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.1 @@ -6827,6 +7435,67 @@ snapshots: dependencies: es6-promise: 4.2.8 + esbuild-register@3.6.0(esbuild@0.25.12): + dependencies: + debug: 4.4.0 + esbuild: 0.25.12 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + escape-string-regexp@2.0.0: {} escape-string-regexp@4.0.0: {} @@ -8070,6 +8739,8 @@ snapshots: postgres-range@1.1.4: {} + postgres@3.4.8: {} + preact@10.25.4: {} prelude-ls@1.2.1: {} @@ -8086,6 +8757,7 @@ snapshots: optionalDependencies: fsevents: 2.3.3 typescript: 5.7.3 + optional: true process-warning@1.0.0: {} @@ -8441,6 +9113,13 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + split-on-first@1.1.0: {} split2@4.2.0: {} diff --git a/prisma/migrations/20250213045633_/migration.sql b/prisma/migrations/20250213045633_/migration.sql deleted file mode 100644 index b3478e3..0000000 --- a/prisma/migrations/20250213045633_/migration.sql +++ /dev/null @@ -1,61 +0,0 @@ --- CreateEnum -CREATE TYPE "Role" AS ENUM ('ADMIN', 'USER'); - --- CreateTable -CREATE TABLE "attendance_room" ( - "id" SERIAL NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "alias" TEXT NOT NULL, - "is_open" BOOLEAN NOT NULL DEFAULT true, - "created_by" INTEGER NOT NULL, - - CONSTRAINT "attendance_room_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "attendance_record" ( - "id" SERIAL NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "attendant_id" INTEGER NOT NULL, - "attendance_room_id" INTEGER NOT NULL, - - CONSTRAINT "attendance_record_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "attendant" ( - "id" SERIAL NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "last_name" TEXT NOT NULL, - "first_name" TEXT NOT NULL, - "uid" TEXT NOT NULL, - "admin" INTEGER, - "wallet_address" TEXT, - - CONSTRAINT "attendant_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "user" ( - "id" SERIAL NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "wallet_address" TEXT NOT NULL, - "role" "Role" NOT NULL DEFAULT 'USER', - - CONSTRAINT "user_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "attendant_uid_key" ON "attendant"("uid"); - --- AddForeignKey -ALTER TABLE "attendance_room" ADD CONSTRAINT "attendance_room_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "attendance_record" ADD CONSTRAINT "attendance_record_attendant_id_fkey" FOREIGN KEY ("attendant_id") REFERENCES "attendant"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "attendance_record" ADD CONSTRAINT "attendance_record_attendance_room_id_fkey" FOREIGN KEY ("attendance_room_id") REFERENCES "attendance_room"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "attendant" ADD CONSTRAINT "attendant_admin_fkey" FOREIGN KEY ("admin") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20250213050630_/migration.sql b/prisma/migrations/20250213050630_/migration.sql deleted file mode 100644 index 212e305..0000000 --- a/prisma/migrations/20250213050630_/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[wallet_address]` on the table `user` will be added. If there are existing duplicate values, this will fail. - -*/ --- CreateIndex -CREATE UNIQUE INDEX "user_wallet_address_key" ON "user"("wallet_address"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index 648c57f..0000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (e.g., Git) -provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index 30e14f9..0000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,64 +0,0 @@ -// This is your Prisma schema file - -generator client { - provider = "prisma-client-js" - previewFeatures = ["driverAdapters"] -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model AttendanceRoom { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) @map("created_at") - alias String - isOpen Boolean @default(true) @map("is_open") - createdBy Int @map("created_by") - creator User @relation(fields: [createdBy], references: [id]) - records AttendanceRecord[] - - @@map("attendance_room") -} - -model AttendanceRecord { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) @map("created_at") - attendantId Int @map("attendant_id") - attendanceRoomId Int @map("attendance_room_id") - attendant Attendant @relation(fields: [attendantId], references: [id]) - attendanceRoom AttendanceRoom @relation(fields: [attendanceRoomId], references: [id]) - - @@map("attendance_record") -} - -model Attendant { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) @map("created_at") - lastName String @map("last_name") - firstName String @map("first_name") - uid String @unique - admin Int? - walletAddress String? @map("wallet_address") - adminUser User? @relation(fields: [admin], references: [id]) - records AttendanceRecord[] - - @@map("attendant") -} - -model User { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) @map("created_at") - walletAddress String @unique @map("wallet_address") - role Role @default(USER) - attendants Attendant[] - rooms AttendanceRoom[] - - @@map("user") -} - -enum Role { - ADMIN - USER -} diff --git a/src/app/(internal)/(protected)/(sidebar)/attendance/[id]/actions.tsx b/src/app/(internal)/(protected)/(sidebar)/attendance/[id]/actions.tsx index fe72d26..c253936 100644 --- a/src/app/(internal)/(protected)/(sidebar)/attendance/[id]/actions.tsx +++ b/src/app/(internal)/(protected)/(sidebar)/attendance/[id]/actions.tsx @@ -4,10 +4,12 @@ import { Config } from "@/config/config"; import { getAttendanceNonceKey } from "@/lib/attendance.constants"; import { isAuthenticated } from "@/lib/auth"; import redis from "@/lib/redis"; -import { prisma } from "@/lib/database"; -import { handlePrismaError } from "@/lib/prisma.error"; +import { db } from "@/lib/db"; +import { attendanceRoom, attendanceRecord, attendant } from "@/lib/db/schema"; +import { handleDatabaseError } from "@/lib/db/error"; import { cookies } from "next/headers"; import { v4 as uuidv4 } from "uuid"; +import { eq, and, gte, desc } from "drizzle-orm"; export async function getAttendance(id: number) { try { @@ -18,20 +20,24 @@ export async function getAttendance(id: number) { return { error: error }; } - const data = await prisma.attendanceRoom.findUnique({ - where: { - id: id, - createdBy: session!.id, - }, - }); - - if (!data) { + const data = await db + .select() + .from(attendanceRoom) + .where( + and( + eq(attendanceRoom.id, id), + eq(attendanceRoom.createdBy, session!.id) + ) + ) + .limit(1); + + if (!data || data.length === 0) { return { error: "Attendance room not found" }; } - return { data }; + return { data: data[0] }; } catch (error) { - return { error: handlePrismaError(error) }; + return { error: handleDatabaseError(error) }; } } @@ -47,36 +53,30 @@ export async function getAttendanceRecordByRoomId(id: number) { const today = new Date(); today.setHours(0, 0, 0, 0); - const rawData = await prisma.attendanceRecord.findMany({ - where: { - attendanceRoomId: id, - attendanceRoom: { - createdBy: session!.id, - }, - createdAt: { - gte: today, - }, - }, - include: { - attendanceRoom: { - select: { - alias: true, - createdAt: true, - isOpen: true, - }, - }, + const rawData = await db + .select({ + id: attendanceRecord.id, + createdAt: attendanceRecord.createdAt, attendant: { - select: { - firstName: true, - lastName: true, - uid: true, - }, + firstName: attendant.firstName, + lastName: attendant.lastName, + uid: attendant.uid, }, - }, - orderBy: { - createdAt: "desc", - }, - }); + }) + .from(attendanceRecord) + .innerJoin( + attendanceRoom, + eq(attendanceRecord.attendanceRoomId, attendanceRoom.id) + ) + .innerJoin(attendant, eq(attendanceRecord.attendantId, attendant.id)) + .where( + and( + eq(attendanceRecord.attendanceRoomId, id), + eq(attendanceRoom.createdBy, session!.id), + gte(attendanceRecord.createdAt, today) + ) + ) + .orderBy(desc(attendanceRecord.createdAt)); // Map the data to match AttendanceRecord interface const data = rawData.map((record) => ({ @@ -91,21 +91,11 @@ export async function getAttendanceRecordByRoomId(id: number) { : null, })); - const count = await prisma.attendanceRecord.count({ - where: { - attendanceRoomId: id, - attendanceRoom: { - createdBy: session!.id, - }, - createdAt: { - gte: today, - }, - }, - }); + const count = data.length; return { count, data }; } catch (error) { - return { error: handlePrismaError(error) }; + return { error: handleDatabaseError(error) }; } } @@ -133,7 +123,7 @@ export async function refreshNonce(id: number) { exp: expirationTime.toISOString(), }; } catch (error) { - return { error: handlePrismaError(error) }; + return { error: handleDatabaseError(error) }; } } @@ -163,18 +153,22 @@ export async function generateAttendanceUrl(id: number) { }; } - const data = await prisma.attendanceRoom.findUnique({ - where: { - id: id, - createdBy: session!.id, - }, - }); - - if (!data) { + const data = await db + .select() + .from(attendanceRoom) + .where( + and( + eq(attendanceRoom.id, id), + eq(attendanceRoom.createdBy, session!.id) + ) + ) + .limit(1); + + if (!data || data.length === 0) { return { error: "Attendance room not found" }; } - if (data.isOpen === false) { + if (data[0].isOpen === false) { return { message: "Attendance room is not open" }; } @@ -194,6 +188,6 @@ export async function generateAttendanceUrl(id: number) { exp: expirationTime.toISOString(), }; } catch (error) { - return { error: handlePrismaError(error) }; + return { error: handleDatabaseError(error) }; } } diff --git a/src/app/(internal)/(protected)/actions.tsx b/src/app/(internal)/(protected)/actions.tsx index 4aa6bf3..e70eef8 100644 --- a/src/app/(internal)/(protected)/actions.tsx +++ b/src/app/(internal)/(protected)/actions.tsx @@ -2,9 +2,11 @@ import { FormValues } from "@/components/pages/createRoom.type"; import { isAuthenticated } from "@/lib/auth"; -import { prisma } from "@/lib/database"; -import { handlePrismaError } from "@/lib/prisma.error"; +import { db } from "@/lib/db"; +import { attendanceRoom } from "@/lib/db/schema"; +import { handleDatabaseError } from "@/lib/db/error"; import { cookies } from "next/headers"; +import { eq, and, desc, count } from "drizzle-orm"; // Custom error type for better error handling type ActionResponse = { @@ -30,18 +32,16 @@ export async function createAttendanceRoom( return { success: false, error: "Unauthorized" }; } - await prisma.attendanceRoom.create({ - data: { - ...data, - createdBy: session.id, - }, + await db.insert(attendanceRoom).values({ + ...data, + createdBy: session.id, }); return { success: true }; } catch (error) { return { success: false, - error: handlePrismaError(error), + error: handleDatabaseError(error), }; } } @@ -59,24 +59,20 @@ export async function getAttendanceRooms(page: number, limit: number) { const skip = (page - 1) * limit; - const [rooms, totalCount] = await Promise.all([ - prisma.attendanceRoom.findMany({ - where: { - createdBy: session.id, - }, - orderBy: { - createdAt: "desc", - }, - skip, - take: limit, - }), - prisma.attendanceRoom.count({ - where: { - createdBy: session.id, - }, - }), - ]); + const rooms = await db + .select() + .from(attendanceRoom) + .where(eq(attendanceRoom.createdBy, session.id)) + .orderBy(desc(attendanceRoom.createdAt)) + .offset(skip) + .limit(limit); + const countResult = await db + .select({ count: count() }) + .from(attendanceRoom) + .where(eq(attendanceRoom.createdBy, session.id)); + + const totalCount = countResult[0].count; const totalPages = Math.ceil(totalCount / limit); return { @@ -84,7 +80,7 @@ export async function getAttendanceRooms(page: number, limit: number) { totalPages, }; } catch (error) { - throw new Error(handlePrismaError(error)); + throw new Error(handleDatabaseError(error)); } } @@ -108,17 +104,18 @@ export async function updateAttendanceRoom( return { success: false, error: "Unauthorized" }; } - const result = await prisma.attendanceRoom.updateMany({ - where: { - id, - createdBy: session.id, - }, - data: { - isOpen: data.isOpen, - }, - }); - - if (result.count === 0) { + const result = await db + .update(attendanceRoom) + .set({ isOpen: data.isOpen }) + .where( + and( + eq(attendanceRoom.id, id), + eq(attendanceRoom.createdBy, session.id) + ) + ) + .returning({ id: attendanceRoom.id }); + + if (result.length === 0) { return { success: false, error: "Room not found or unauthorized" }; } @@ -126,7 +123,7 @@ export async function updateAttendanceRoom( } catch (error) { return { success: false, - error: handlePrismaError(error), + error: handleDatabaseError(error), }; } } @@ -149,14 +146,17 @@ export async function deleteAttendanceRoom( return { success: false, error: "Unauthorized" }; } - const result = await prisma.attendanceRoom.deleteMany({ - where: { - id, - createdBy: session.id, - }, - }); - - if (result.count === 0) { + const result = await db + .delete(attendanceRoom) + .where( + and( + eq(attendanceRoom.id, id), + eq(attendanceRoom.createdBy, session.id) + ) + ) + .returning({ id: attendanceRoom.id }); + + if (result.length === 0) { return { success: false, error: "Room not found or unauthorized" }; } @@ -164,7 +164,7 @@ export async function deleteAttendanceRoom( } catch (error) { return { success: false, - error: handlePrismaError(error), + error: handleDatabaseError(error), }; } } @@ -189,17 +189,18 @@ export async function updateAttendanceRoomName( return { success: false, error: "Unauthorized" }; } - const result = await prisma.attendanceRoom.updateMany({ - where: { - id, - createdBy: session.id, - }, - data: { - alias: data.alias, - }, - }); - - if (result.count === 0) { + const result = await db + .update(attendanceRoom) + .set({ alias: data.alias }) + .where( + and( + eq(attendanceRoom.id, id), + eq(attendanceRoom.createdBy, session.id) + ) + ) + .returning({ id: attendanceRoom.id }); + + if (result.length === 0) { return { success: false, error: "Room not found or unauthorized" }; } @@ -207,7 +208,7 @@ export async function updateAttendanceRoomName( } catch (error) { return { success: false, - error: handlePrismaError(error), + error: handleDatabaseError(error), }; } } diff --git a/src/app/(public)/attendance/[id]/take/actions.tsx b/src/app/(public)/attendance/[id]/take/actions.tsx index 2d857e8..ee62589 100644 --- a/src/app/(public)/attendance/[id]/take/actions.tsx +++ b/src/app/(public)/attendance/[id]/take/actions.tsx @@ -4,11 +4,13 @@ import { getAttendantSignInMessage } from "@/config/config"; import { signInAsAttendant as signInAsAttendantLib } from "@/lib/attendance"; import { getAttendanceNonceKey } from "@/lib/attendance.constants"; import redis from "@/lib/redis"; -import { prisma } from "@/lib/database"; -import { handlePrismaError } from "@/lib/prisma.error"; +import { db } from "@/lib/db"; +import { attendant, attendanceRecord, attendanceRoom, user } from "@/lib/db/schema"; +import { handleDatabaseError } from "@/lib/db/error"; import { verifyMessage } from "ethers"; import { cookies } from "next/headers"; import { SessionResponse } from "web3-connect-react"; +import { eq, and, gte, isNull } from "drizzle-orm"; interface SignInAsAttendantUser { firstName: string; @@ -19,36 +21,30 @@ interface SignInAsAttendantUser { export async function getAllAttendant(roomId: number) { try { - const attendants = await prisma.attendant.findMany({ - where: { - adminUser: { - rooms: { - some: { - id: roomId, - }, - }, - }, - }, - select: { - id: true, - uid: true, - firstName: true, - lastName: true, - walletAddress: true, - }, - }); - - const data = attendants.map((attendant) => ({ - userId: attendant.uid, - id: attendant.id, - firstName: attendant.firstName, - lastName: attendant.lastName, - disabled: !!attendant.walletAddress, + const attendants = await db + .select({ + id: attendant.id, + uid: attendant.uid, + firstName: attendant.firstName, + lastName: attendant.lastName, + walletAddress: attendant.walletAddress, + }) + .from(attendant) + .innerJoin(user, eq(attendant.admin, user.id)) + .innerJoin(attendanceRoom, eq(attendanceRoom.createdBy, user.id)) + .where(eq(attendanceRoom.id, roomId)); + + const data = attendants.map((att) => ({ + userId: att.uid, + id: att.id, + firstName: att.firstName, + lastName: att.lastName, + disabled: !!att.walletAddress, })); return { data }; } catch (error) { - return { error: handlePrismaError(error) }; + return { error: handleDatabaseError(error) }; } } @@ -61,33 +57,46 @@ export async function getAttendantByWalletAddress( return { data: undefined }; } - const query: any = { - where: { - walletAddress: walletAddress, - }, - select: { - id: true, - uid: true, - firstName: true, - lastName: true, - walletAddress: true, - }, - }; - + let query; + // If roomId is provided, only find attendants in that specific room if (roomId) { - query.where.adminUser = { - rooms: { - some: { - id: roomId, - }, - }, - }; + query = db + .select({ + id: attendant.id, + uid: attendant.uid, + firstName: attendant.firstName, + lastName: attendant.lastName, + walletAddress: attendant.walletAddress, + }) + .from(attendant) + .innerJoin(user, eq(attendant.admin, user.id)) + .innerJoin(attendanceRoom, eq(attendanceRoom.createdBy, user.id)) + .where( + and( + eq(attendant.walletAddress, walletAddress), + eq(attendanceRoom.id, roomId) + ) + ) + .limit(1); + } else { + query = db + .select({ + id: attendant.id, + uid: attendant.uid, + firstName: attendant.firstName, + lastName: attendant.lastName, + walletAddress: attendant.walletAddress, + }) + .from(attendant) + .where(eq(attendant.walletAddress, walletAddress)) + .limit(1); } - const data = await prisma.attendant.findFirst(query); + const result = await query; + const data = result[0]; - if (data === null) { + if (!data) { return { data: undefined }; } @@ -101,7 +110,7 @@ export async function getAttendantByWalletAddress( }, }; } catch (error) { - return { error: handlePrismaError(error) }; + return { error: handleDatabaseError(error) }; } } @@ -117,19 +126,21 @@ export async function hasAttendantTakenAttendanceForToday( const today = new Date(); today.setHours(0, 0, 0, 0); - const data = await prisma.attendanceRecord.findFirst({ - where: { - attendantId, - attendanceRoomId: roomId, - createdAt: { - gte: today, - }, - }, - }); - - return { data }; + const result = await db + .select() + .from(attendanceRecord) + .where( + and( + eq(attendanceRecord.attendantId, attendantId), + eq(attendanceRecord.attendanceRoomId, roomId), + gte(attendanceRecord.createdAt, today) + ) + ) + .limit(1); + + return { data: result[0] }; } catch (error) { - return { error: handlePrismaError(error) }; + return { error: handleDatabaseError(error) }; } } @@ -153,28 +164,28 @@ export async function getAttendantByWalletAddressForRoom( return { data: undefined }; } - const data = await prisma.attendant.findFirst({ - where: { - walletAddress: walletAddress, - // Only include attendants that belong to the specified room's admin - adminUser: { - rooms: { - some: { - id: roomId, - }, - }, - }, - }, - select: { - id: true, - uid: true, - firstName: true, - lastName: true, - walletAddress: true, - }, - }); - - if (data === null) { + const result = await db + .select({ + id: attendant.id, + uid: attendant.uid, + firstName: attendant.firstName, + lastName: attendant.lastName, + walletAddress: attendant.walletAddress, + }) + .from(attendant) + .innerJoin(user, eq(attendant.admin, user.id)) + .innerJoin(attendanceRoom, eq(attendanceRoom.createdBy, user.id)) + .where( + and( + eq(attendant.walletAddress, walletAddress), + eq(attendanceRoom.id, roomId) + ) + ) + .limit(1); + + const data = result[0]; + + if (!data) { return { data: undefined }; } @@ -188,7 +199,7 @@ export async function getAttendantByWalletAddressForRoom( }, }; } catch (error) { - return { error: handlePrismaError(error) }; + return { error: handleDatabaseError(error) }; } } @@ -232,16 +243,20 @@ export async function takeAttendance( ); // Get the attendant we're trying to register - const attendant = await prisma.attendant.findFirst({ - where: { id: user.id }, - }); + const attendantRecords = await db + .select() + .from(attendant) + .where(eq(attendant.id, user.id)) + .limit(1); - if (!attendant) { + const foundAttendant = attendantRecords[0]; + + if (!foundAttendant) { return { error: "Attendant not found" }; } // If attendant has no wallet address, register it - if (attendant.walletAddress === null) { + if (foundAttendant.walletAddress === null) { // If this wallet is already registered to someone else if ( userWithWalletAddress.data?.address && @@ -253,45 +268,42 @@ export async function takeAttendance( }; } - const updatedAttendant = await prisma.attendant.update({ - where: { - id: user.id, - walletAddress: null, - }, - data: { - walletAddress: address, - }, - }); + const updatedAttendant = await db + .update(attendant) + .set({ walletAddress: address }) + .where( + and( + eq(attendant.id, user.id), + isNull(attendant.walletAddress) + ) + ) + .returning(); // Use the updated attendant's ID - const userId = updatedAttendant.id; + const attendantId = updatedAttendant[0].id; // Add the attendance record - await prisma.attendanceRecord.create({ - data: { - attendanceRoomId: roomId, - attendantId: userId, - }, + await db.insert(attendanceRecord).values({ + attendanceRoomId: roomId, + attendantId: attendantId, }); return { error: null }; } // If this attendant already has a wallet but it's different - else if (attendant.walletAddress !== address) { + else if (foundAttendant.walletAddress !== address) { return { error: "This user already has a registered wallet address" }; } // If this attendant has this wallet address, just record attendance else { - await prisma.attendanceRecord.create({ - data: { - attendanceRoomId: roomId, - attendantId: attendant.id, - }, + await db.insert(attendanceRecord).values({ + attendanceRoomId: roomId, + attendantId: foundAttendant.id, }); return { error: null }; } } catch (error) { - return { error: handlePrismaError(error) }; + return { error: handleDatabaseError(error) }; } } diff --git a/src/components/pages/attendance/AttendanceDetailView.tsx b/src/components/pages/attendance/AttendanceDetailView.tsx index 1c47f67..a0d1373 100644 --- a/src/components/pages/attendance/AttendanceDetailView.tsx +++ b/src/components/pages/attendance/AttendanceDetailView.tsx @@ -6,7 +6,7 @@ import Spinner from "@/components/ui/spinner"; import { useToast } from "@/hooks/use-toast"; import { useAttendanceRecord } from "@/hooks/useAttendanceRecord"; import { useAttendanceUrl } from "@/hooks/useAttendanceUrl"; -import { AttendanceRoom } from "@prisma/client"; +import { attendanceRoom } from "@/lib/db/schema"; import { motion } from "motion/react"; import dynamic from "next/dynamic"; import { useState } from "react"; @@ -15,6 +15,8 @@ import AttendanceRecordList from "../../attendance/AttendanceRecordList"; import { Card } from "../../ui/card"; import RoomActions from "./RoomActions"; +type AttendanceRoom = typeof attendanceRoom.$inferSelect; + const QRCodePanel = dynamic( () => import("../../attendance/QRCodePanel").then((mod) => mod.default), { diff --git a/src/components/pages/attendance/AttendanceList.tsx b/src/components/pages/attendance/AttendanceList.tsx index 47a200b..ba3c456 100644 --- a/src/components/pages/attendance/AttendanceList.tsx +++ b/src/components/pages/attendance/AttendanceList.tsx @@ -12,7 +12,9 @@ import { useToast } from "@/hooks/use-toast"; import { useRouter } from "next/navigation"; import UpdateRoomDialog from "../UpdateRoomDialog"; import Link from "next/link"; -import { AttendanceRoom } from "@prisma/client"; +import { attendanceRoom } from "@/lib/db/schema"; + +type AttendanceRoom = typeof attendanceRoom.$inferSelect; export function AttendanceRoomList({ rooms }: { rooms: AttendanceRoom[] }) { return ( diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 7dd8d13..ad12616 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -9,8 +9,22 @@ import { createSecretKey } from "crypto"; import redis from "./redis"; import { Config, getAdminSignInMessage } from "@/config/config"; import dayjs from "dayjs"; -import { prisma } from "./database"; -import { Role, User } from "@prisma/client"; +import { db } from "./db"; +import { user } from "./db/schema"; +import { eq } from "drizzle-orm"; + +// Type definitions based on schema +export type User = { + id: number; + createdAt: Date; + walletAddress: string; + role: "ADMIN" | "USER"; +}; + +export const Role = { + ADMIN: "ADMIN" as const, + USER: "USER" as const, +}; export async function isAuthenticated(cookie: ReadonlyRequestCookies) { const session = await getSession(cookie); @@ -129,21 +143,23 @@ async function adminOnly( walletAddress: string ): Promise<[boolean, User | null, string | null]> { try { - const user = await prisma.user.findFirst({ - where: { - walletAddress: walletAddress, - }, - }); + const users = await db + .select() + .from(user) + .where(eq(user.walletAddress, walletAddress)) + .limit(1); + + const foundUser = users[0]; - if (!user) { + if (!foundUser) { return [false, null, "User not found"]; } - if (user.role !== Role.ADMIN) { + if (foundUser.role !== Role.ADMIN) { return [false, null, "User is not an admin"]; } - return [true, user, null]; + return [true, foundUser, null]; } catch (error) { console.error(error); return [false, null, "Database error occurred"]; diff --git a/src/lib/database.ts b/src/lib/database.ts deleted file mode 100644 index b3a2a56..0000000 --- a/src/lib/database.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Pool, neonConfig } from "@neondatabase/serverless"; -import { PrismaNeon } from "@prisma/adapter-neon"; -import { PrismaClient } from "@prisma/client"; -import ws from "ws"; - -export const prisma = getPrismaClient(); - -function getPrismaClient() { - if (process.env.IS_TEST === "true") { - return new PrismaClient(); - } - - neonConfig.webSocketConstructor = ws; - const connectionString = `${process.env.DATABASE_URL}`; - const pool = new Pool({ connectionString }); - const adapter = new PrismaNeon(pool); - return new PrismaClient({ adapter }); -} diff --git a/src/lib/db/error.ts b/src/lib/db/error.ts new file mode 100644 index 0000000..f816b1e --- /dev/null +++ b/src/lib/db/error.ts @@ -0,0 +1,22 @@ +import { DatabaseError } from "pg"; + +/** + * Handle database errors and return appropriate error messages + */ +export function handleDatabaseError(error: unknown): string { + if (error instanceof DatabaseError) { + switch (error.code) { + case "23505": // unique_violation + return "A record with this value already exists."; + case "23503": // foreign_key_violation + return "Operation failed due to foreign key constraint."; + case "23502": // not_null_violation + return "Required field is missing."; + default: + return `Database error: ${error.code}`; + } + } + return error instanceof Error + ? error.message + : "An unexpected error occurred"; +} diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts new file mode 100644 index 0000000..a0ee5e2 --- /dev/null +++ b/src/lib/db/index.ts @@ -0,0 +1,20 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import * as schema from "./schema"; + +export const db = getDrizzleClient(); + +function getDrizzleClient() { + // For test environment, use the test database URL directly + if (process.env.IS_TEST === "true") { + const queryClient = postgres(process.env.DATABASE_URL!); + return drizzle(queryClient, { schema }); + } + + // For production, use the Neon serverless adapter with WebSockets + const queryClient = postgres(process.env.DATABASE_URL!, { + prepare: false, + }); + + return drizzle(queryClient, { schema }); +} diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts new file mode 100644 index 0000000..283bc18 --- /dev/null +++ b/src/lib/db/schema.ts @@ -0,0 +1,74 @@ +import { pgTable, serial, text, timestamp, boolean, integer, pgEnum } from "drizzle-orm/pg-core"; +import { relations } from "drizzle-orm"; + +// Enum for Role +export const roleEnum = pgEnum("Role", ["ADMIN", "USER"]); + +// User table +export const user = pgTable("user", { + id: serial("id").primaryKey(), + createdAt: timestamp("created_at").notNull().defaultNow(), + walletAddress: text("wallet_address").notNull().unique(), + role: roleEnum("role").notNull().default("USER"), +}); + +// AttendanceRoom table +export const attendanceRoom = pgTable("attendance_room", { + id: serial("id").primaryKey(), + createdAt: timestamp("created_at").notNull().defaultNow(), + alias: text("alias").notNull(), + isOpen: boolean("is_open").notNull().default(true), + createdBy: integer("created_by").notNull().references(() => user.id), +}); + +// Attendant table +export const attendant = pgTable("attendant", { + id: serial("id").primaryKey(), + createdAt: timestamp("created_at").notNull().defaultNow(), + lastName: text("last_name").notNull(), + firstName: text("first_name").notNull(), + uid: text("uid").notNull().unique(), + admin: integer("admin").references(() => user.id), + walletAddress: text("wallet_address"), +}); + +// AttendanceRecord table +export const attendanceRecord = pgTable("attendance_record", { + id: serial("id").primaryKey(), + createdAt: timestamp("created_at").notNull().defaultNow(), + attendantId: integer("attendant_id").notNull().references(() => attendant.id), + attendanceRoomId: integer("attendance_room_id").notNull().references(() => attendanceRoom.id), +}); + +// Relations +export const userRelations = relations(user, ({ many }) => ({ + attendants: many(attendant), + rooms: many(attendanceRoom), +})); + +export const attendanceRoomRelations = relations(attendanceRoom, ({ one, many }) => ({ + creator: one(user, { + fields: [attendanceRoom.createdBy], + references: [user.id], + }), + records: many(attendanceRecord), +})); + +export const attendantRelations = relations(attendant, ({ one, many }) => ({ + adminUser: one(user, { + fields: [attendant.admin], + references: [user.id], + }), + records: many(attendanceRecord), +})); + +export const attendanceRecordRelations = relations(attendanceRecord, ({ one }) => ({ + attendant: one(attendant, { + fields: [attendanceRecord.attendantId], + references: [attendant.id], + }), + attendanceRoom: one(attendanceRoom, { + fields: [attendanceRecord.attendanceRoomId], + references: [attendanceRoom.id], + }), +})); diff --git a/src/lib/prisma.error.ts b/src/lib/prisma.error.ts deleted file mode 100644 index a8d6996..0000000 --- a/src/lib/prisma.error.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Prisma } from "@prisma/client"; - -/** - * Handle Prisma errors and return appropriate error messages - */ -export function handlePrismaError(error: unknown): string { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - switch (error.code) { - case "P2002": - return "A room with this name already exists."; - case "P2025": - return "Record not found."; - case "P2003": - return "Operation failed due to foreign key constraint."; - default: - return `Database error: ${error.code}`; - } - } - return error instanceof Error - ? error.message - : "An unexpected error occurred"; -} diff --git a/tests/attendance/create-room.spec.ts b/tests/attendance/create-room.spec.ts index a2713e7..af76cd2 100644 --- a/tests/attendance/create-room.spec.ts +++ b/tests/attendance/create-room.spec.ts @@ -1,9 +1,10 @@ -import { prisma } from "@/lib/database"; +import { db } from "@/lib/db"; +import { user as userTable, attendanceRoom as attendanceRoomTable } from "@/lib/db/schema"; import { expect, test } from "@playwright/test"; import { ethers } from "ethers"; import { FastifyInstance } from "fastify"; import { signInWithWallet } from "../helpers/signInHelper"; -import { User } from "@prisma/client"; +import { User } from "@/lib/auth"; import { createMetaMaskController } from "../metamaskServer"; const adminWallet = ethers.Wallet.createRandom(); let server: FastifyInstance; @@ -13,25 +14,22 @@ let admin: User; test.beforeEach(async () => { // add admin wallet to database - admin = await prisma.user.create({ - data: { - walletAddress: adminWallet.address, - role: "ADMIN", - }, - }); + const result = await db.insert(userTable).values({ + walletAddress: adminWallet.address, + role: "ADMIN", + }).returning(); + admin = result[0]; // add attendant wallet to database - await prisma.user.create({ - data: { - walletAddress: attendantWallet.address, - role: "USER", - }, + await db.insert(userTable).values({ + walletAddress: attendantWallet.address, + role: "USER", }); }); test.afterEach(async () => { - await prisma.attendanceRoom.deleteMany(); - await prisma.user.deleteMany(); + await db.delete(attendanceRoomTable); + await db.delete(userTable); await server.close(); }); @@ -116,12 +114,12 @@ test.describe("room", () => { test.describe("pagination", () => { test("create a room", async ({ page }) => { // write 21 rooms in the database - await prisma.attendanceRoom.createMany({ - data: Array.from({ length: 21 }, (_, i) => ({ + await db.insert(attendanceRoomTable).values( + Array.from({ length: 21 }, (_, i) => ({ alias: `Test Room ${i + 1}`, createdBy: admin.id, - })), - }); + })) + ); const response = await createMetaMaskController(); server = response.server; diff --git a/tests/attendance/take-attendance.spec.ts b/tests/attendance/take-attendance.spec.ts index c49dca9..06328b6 100644 --- a/tests/attendance/take-attendance.spec.ts +++ b/tests/attendance/take-attendance.spec.ts @@ -1,10 +1,19 @@ -import { prisma } from "@/lib/database"; +import { db } from "@/lib/db"; +import { + user, + attendant as attendantTable, + attendanceRoom as attendanceRoomTable, + attendanceRecord as attendanceRecordTable +} from "@/lib/db/schema"; +import { eq } from "drizzle-orm"; import { expect, Page, test } from "@playwright/test"; import { ethers } from "ethers"; import { FastifyInstance } from "fastify"; import { signInWithWallet } from "../helpers/signInHelper"; -import { User, Attendant } from "@prisma/client"; +import { User } from "@/lib/auth"; import { createMetaMaskController } from "../metamaskServer"; + +type Attendant = typeof attendantTable.$inferSelect; const adminWallet = ethers.Wallet.createRandom(); let server: FastifyInstance; @@ -18,47 +27,36 @@ let attendant2: Attendant; test.beforeEach(async () => { // add admin wallet to database - admin = await prisma.user.create({ - data: { - walletAddress: adminWallet.address, - role: "ADMIN", - }, - }); + const adminResult = await db.insert(user).values({ + walletAddress: adminWallet.address, + role: "ADMIN", + }).returning(); + admin = adminResult[0]; // add attendant wallet to database - attendant = await prisma.attendant.create({ - data: { - firstName: "Test", - lastName: "Attendant", - uid: "1", - adminUser: { - connect: { - id: admin.id, - }, - }, - }, - }); + const attendantResult = await db.insert(attendantTable).values({ + firstName: "Test", + lastName: "Attendant", + uid: "1", + admin: admin.id, + }).returning(); + attendant = attendantResult[0]; // create attendant - attendant2 = await prisma.attendant.create({ - data: { - firstName: "Test2", - lastName: "Attendant2", - uid: "2", - adminUser: { - connect: { - id: admin.id, - }, - }, - }, - }); + const attendant2Result = await db.insert(attendantTable).values({ + firstName: "Test2", + lastName: "Attendant2", + uid: "2", + admin: admin.id, + }).returning(); + attendant2 = attendant2Result[0]; }); test.afterEach(async () => { - await prisma.attendanceRecord.deleteMany(); - await prisma.attendanceRoom.deleteMany(); - await prisma.attendant.deleteMany(); - await prisma.user.deleteMany(); + await db.delete(attendanceRecordTable); + await db.delete(attendanceRoomTable); + await db.delete(attendantTable); + await db.delete(user); await server?.close(); }); @@ -70,14 +68,13 @@ test.describe("room", () => { response.controller.setWallet(adminWallet); await signInWithWallet(page); - const room = await prisma.attendanceRoom.create({ - data: { - alias: "Test Room", - createdBy: admin.id, - }, - }); + const roomResult = await db.insert(attendanceRoomTable).values({ + alias: "Test Room", + createdBy: admin.id, + }).returning(); + return { - room, + room: roomResult[0], controller: response.controller, }; } @@ -138,17 +135,14 @@ test.describe("room", () => { context, }) => { // update attendant 1 with wallet address - await prisma.attendant.update({ - where: { id: attendant.id }, - data: { walletAddress: attendantWallet.address }, - }); + await db.update(attendantTable).set({ + walletAddress: attendantWallet.address + }).where(eq(attendantTable.id, attendant.id)); const { room, controller } = await signInAndCreateRoom(page); - await prisma.attendanceRecord.create({ - data: { - attendantId: attendant.id, - attendanceRoomId: room.id, - }, + await db.insert(attendanceRecordTable).values({ + attendantId: attendant.id, + attendanceRoomId: room.id, }); // go to the room page @@ -185,10 +179,9 @@ test.describe("room", () => { context, }) => { // update attendant 1 with wallet address - await prisma.attendant.update({ - where: { id: attendant.id }, - data: { walletAddress: attendantWallet.address }, - }); + await db.update(attendantTable).set({ + walletAddress: attendantWallet.address + }).where(eq(attendantTable.id, attendant.id)); const { room, controller } = await signInAndCreateRoom(page); @@ -233,27 +226,23 @@ test.describe("room", () => { context, }) => { // update attendant 1 with wallet address - await prisma.attendant.update({ - where: { id: attendant.id }, - data: { walletAddress: attendantWallet.address }, - }); + await db.update(attendantTable).set({ + walletAddress: attendantWallet.address + }).where(eq(attendantTable.id, attendant.id)); // Create first room and take attendance const { room: room1, controller } = await signInAndCreateRoom(page); - await prisma.attendanceRecord.create({ - data: { - attendantId: attendant.id, - attendanceRoomId: room1.id, - }, + await db.insert(attendanceRecordTable).values({ + attendantId: attendant.id, + attendanceRoomId: room1.id, }); // Create second room - const room2 = await prisma.attendanceRoom.create({ - data: { - alias: "Test Room 2", - createdBy: admin.id, - }, - }); + const room2Result = await db.insert(attendanceRoomTable).values({ + alias: "Test Room 2", + createdBy: admin.id, + }).returning(); + const room2 = room2Result[0]; // Go to the second room page await page.goto(`/attendance/${room2.id}`); @@ -289,11 +278,9 @@ test.describe("room", () => { await expect(attendanceTakenMessage).toBeVisible(); // Verify that attendance records exist in both rooms - const attendanceRecords = await prisma.attendanceRecord.findMany({ - where: { - attendantId: attendant.id, - }, - }); + const attendanceRecords = await db.select().from(attendanceRecordTable).where( + eq(attendanceRecordTable.attendantId, attendant.id) + ); expect(attendanceRecords.length).toBe(2); expect(attendanceRecords.some(record => record.attendanceRoomId === room1.id)).toBeTruthy(); diff --git a/tests/authentication-admin/auth.spec.ts b/tests/authentication-admin/auth.spec.ts index 454699f..cbfad22 100644 --- a/tests/authentication-admin/auth.spec.ts +++ b/tests/authentication-admin/auth.spec.ts @@ -1,9 +1,11 @@ -import { prisma } from "@/lib/database"; +import { db } from "@/lib/db"; +import { user as userTable } from "@/lib/db/schema"; import { expect, test } from "@playwright/test"; import { ethers } from "ethers"; import { FastifyInstance } from "fastify"; import { signInWithWallet } from "../helpers/signInHelper"; import { createMetaMaskController } from "../metamaskServer"; +import { sql } from "drizzle-orm"; const adminWallet = ethers.Wallet.createRandom(); let server: FastifyInstance; @@ -12,24 +14,20 @@ const [attendantWallet] = [ethers.Wallet.createRandom()]; test.beforeEach(async () => { // add admin wallet to database - await prisma.user.create({ - data: { - walletAddress: adminWallet.address, - role: "ADMIN", - }, + await db.insert(userTable).values({ + walletAddress: adminWallet.address, + role: "ADMIN", }); // add attendant wallet to database - await prisma.user.create({ - data: { - walletAddress: attendantWallet.address, - role: "USER", - }, + await db.insert(userTable).values({ + walletAddress: attendantWallet.address, + role: "USER", }); }); test.afterEach(async () => { - await prisma.user.deleteMany(); + await db.delete(userTable); await server.close(); });