diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..4ab5d5e --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit' + +export default defineConfig({ + schema: './server/database/schema.ts', + out: './server/database/migrations', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}) diff --git a/package.json b/package.json index 16bef2c..9d36367 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,18 @@ "postinstall": "nuxt prepare", "lint": "eslint .", "format": "prettier --write .", - "typecheck": "nuxt typecheck" + "typecheck": "nuxt typecheck", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio", + "db:push": "drizzle-kit push" }, "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "drizzle-orm": "^0.44.7", "nuxt": "^4.2.0", + "postgres": "^3.4.7", "reka-ui": "^2.6.0", "tailwind-merge": "^3.3.1", "vue": "^3.5.22", @@ -25,6 +31,7 @@ "@nuxt/eslint": "^1.10.0", "@nuxtjs/tailwindcss": "^6.14.0", "@types/node": "^22.10.0", + "drizzle-kit": "^0.31.6", "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9ae368..0fb6911 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,15 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + drizzle-orm: + specifier: ^0.44.7 + version: 0.44.7(postgres@3.4.7) nuxt: specifier: ^4.2.0 - version: 4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1) + version: 4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)))(drizzle-orm@0.44.7(postgres@3.4.7))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1) + postgres: + specifier: ^3.4.7 + version: 3.4.7 reka-ui: specifier: ^2.6.0 version: 2.6.0(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)) @@ -39,6 +45,9 @@ importers: '@types/node': specifier: ^22.10.0 version: 22.18.13 + drizzle-kit: + specifier: ^0.31.6 + version: 0.31.6 eslint: specifier: ^9.38.0 version: 9.38.0(jiti@2.6.1) @@ -212,6 +221,9 @@ packages: peerDependencies: postcss-selector-parser: ^7.0.0 + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@dxup/nuxt@0.2.0': resolution: {integrity: sha512-tUS2040HEiGwjwZ8hTczfuRoiXSOuA+ATPXO9Bllf03nHHj1lSlmaAyVJHFsSXL5Os5NZqimNAZ1iDed7VElzA==} @@ -235,102 +247,206 @@ packages: resolution: {integrity: sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==} engines: {node: '>=10'} + '@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.11': resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} 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.11': resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} 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.11': resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} 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.11': resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} 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.11': resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} 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.11': resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} 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.11': resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} 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.11': resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} 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.11': resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} 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.11': resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} 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.11': resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} 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.11': resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} 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.11': resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} 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.11': resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} 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.11': resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} 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.11': resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} 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.11': resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} engines: {node: '>=18'} @@ -343,6 +459,12 @@ packages: 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.11': resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} engines: {node: '>=18'} @@ -355,6 +477,12 @@ packages: 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.11': resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} engines: {node: '>=18'} @@ -367,24 +495,48 @@ packages: 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.11': resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} 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.11': resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} 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.11': resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} 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.11': resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} engines: {node: '>=18'} @@ -2212,6 +2364,102 @@ packages: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} + drizzle-kit@0.31.6: + resolution: {integrity: sha512-/B4e/4pwnx25QwD5xXgdpo1S+077a2VZdosXbItE/oNmUgQwZydGDz9qJYmnQl/b+5IX0rLfwRhrPnroGtrg8Q==} + hasBin: true + + drizzle-orm@0.44.7: + resolution: {integrity: sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==} + 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'} @@ -2267,6 +2515,16 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + 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.11: resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} engines: {node: '>=18'} @@ -3701,6 +3959,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4823,6 +5085,8 @@ snapshots: dependencies: postcss-selector-parser: 7.1.0 + '@drizzle-team/brocli@0.10.2': {} + '@dxup/nuxt@0.2.0(magicast@0.5.0)': dependencies: '@dxup/unimport': 0.1.0 @@ -4861,81 +5125,157 @@ snapshots: '@es-joy/resolve.exports@1.2.0': {} + '@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.13.0 + '@esbuild/aix-ppc64@0.25.11': optional: true + '@esbuild/android-arm64@0.18.20': + optional: true + '@esbuild/android-arm64@0.25.11': optional: true + '@esbuild/android-arm@0.18.20': + optional: true + '@esbuild/android-arm@0.25.11': optional: true + '@esbuild/android-x64@0.18.20': + optional: true + '@esbuild/android-x64@0.25.11': optional: true + '@esbuild/darwin-arm64@0.18.20': + optional: true + '@esbuild/darwin-arm64@0.25.11': optional: true + '@esbuild/darwin-x64@0.18.20': + optional: true + '@esbuild/darwin-x64@0.25.11': optional: true + '@esbuild/freebsd-arm64@0.18.20': + optional: true + '@esbuild/freebsd-arm64@0.25.11': optional: true + '@esbuild/freebsd-x64@0.18.20': + optional: true + '@esbuild/freebsd-x64@0.25.11': optional: true + '@esbuild/linux-arm64@0.18.20': + optional: true + '@esbuild/linux-arm64@0.25.11': optional: true + '@esbuild/linux-arm@0.18.20': + optional: true + '@esbuild/linux-arm@0.25.11': optional: true + '@esbuild/linux-ia32@0.18.20': + optional: true + '@esbuild/linux-ia32@0.25.11': optional: true + '@esbuild/linux-loong64@0.18.20': + optional: true + '@esbuild/linux-loong64@0.25.11': optional: true + '@esbuild/linux-mips64el@0.18.20': + optional: true + '@esbuild/linux-mips64el@0.25.11': optional: true + '@esbuild/linux-ppc64@0.18.20': + optional: true + '@esbuild/linux-ppc64@0.25.11': optional: true + '@esbuild/linux-riscv64@0.18.20': + optional: true + '@esbuild/linux-riscv64@0.25.11': optional: true + '@esbuild/linux-s390x@0.18.20': + optional: true + '@esbuild/linux-s390x@0.25.11': optional: true + '@esbuild/linux-x64@0.18.20': + optional: true + '@esbuild/linux-x64@0.25.11': optional: true '@esbuild/netbsd-arm64@0.25.11': optional: true + '@esbuild/netbsd-x64@0.18.20': + optional: true + '@esbuild/netbsd-x64@0.25.11': optional: true '@esbuild/openbsd-arm64@0.25.11': optional: true + '@esbuild/openbsd-x64@0.18.20': + optional: true + '@esbuild/openbsd-x64@0.25.11': optional: true '@esbuild/openharmony-arm64@0.25.11': optional: true + '@esbuild/sunos-x64@0.18.20': + optional: true + '@esbuild/sunos-x64@0.25.11': optional: true + '@esbuild/win32-arm64@0.18.20': + optional: true + '@esbuild/win32-arm64@0.25.11': optional: true + '@esbuild/win32-ia32@0.18.20': + optional: true + '@esbuild/win32-ia32@0.25.11': optional: true + '@esbuild/win32-x64@0.18.20': + optional: true + '@esbuild/win32-x64@0.25.11': optional: true @@ -5412,7 +5752,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/nitro-server@4.2.0(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.3)': + '@nuxt/nitro-server@4.2.0(db0@0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)))(drizzle-orm@0.44.7(postgres@3.4.7))(ioredis@5.8.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)))(drizzle-orm@0.44.7(postgres@3.4.7))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.3)': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.2.0(magicast@0.5.0) @@ -5429,15 +5769,15 @@ snapshots: impound: 1.0.0 klona: 2.0.6 mocked-exports: 0.1.1 - nitropack: 2.12.9 - nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1) + nitropack: 2.12.9(drizzle-orm@0.44.7(postgres@3.4.7)) + nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)))(drizzle-orm@0.44.7(postgres@3.4.7))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1) pathe: 2.0.3 pkg-types: 2.3.0 radix3: 1.1.2 std-env: 3.10.0 ufo: 1.6.1 unctx: 2.4.1 - unstorage: 1.17.1(db0@0.3.4)(ioredis@5.8.2) + unstorage: 1.17.1(db0@0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)))(ioredis@5.8.2) vue: 3.5.22(typescript@5.9.3) vue-bundle-renderer: 2.2.0 vue-devtools-stub: 0.1.0 @@ -5501,7 +5841,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/vite-builder@4.2.0(@types/node@22.18.13)(eslint@9.38.0(jiti@2.6.1))(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)': + '@nuxt/vite-builder@4.2.0(@types/node@22.18.13)(eslint@9.38.0(jiti@2.6.1))(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)))(drizzle-orm@0.44.7(postgres@3.4.7))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)': dependencies: '@nuxt/kit': 4.2.0(magicast@0.5.0) '@rollup/plugin-replace': 6.0.3(rollup@4.52.5) @@ -5521,7 +5861,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.0 mocked-exports: 0.1.1 - nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1) + nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)))(drizzle-orm@0.44.7(postgres@3.4.7))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1) pathe: 2.0.3 pkg-types: 2.3.0 postcss: 8.5.6 @@ -6857,7 +7197,9 @@ snapshots: csstype@3.1.3: {} - db0@0.3.4: {} + db0@0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)): + optionalDependencies: + drizzle-orm: 0.44.7(postgres@3.4.7) debug@3.2.7: dependencies: @@ -6936,6 +7278,19 @@ snapshots: dotenv@17.2.3: {} + drizzle-kit@0.31.6: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.11 + esbuild-register: 3.6.0(esbuild@0.25.11) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.44.7(postgres@3.4.7): + optionalDependencies: + postgres: 3.4.7 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6974,6 +7329,38 @@ snapshots: dependencies: es-errors: 1.3.0 + esbuild-register@3.6.0(esbuild@0.25.11): + dependencies: + debug: 4.4.3 + esbuild: 0.25.11 + 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.11: optionalDependencies: '@esbuild/aix-ppc64': 0.25.11 @@ -7989,7 +8376,7 @@ snapshots: negotiator@0.6.3: {} - nitropack@2.12.9: + nitropack@2.12.9(drizzle-orm@0.44.7(postgres@3.4.7)): dependencies: '@cloudflare/kv-asset-handler': 0.4.0 '@rollup/plugin-alias': 5.1.1(rollup@4.52.5) @@ -8010,7 +8397,7 @@ snapshots: cookie-es: 2.0.0 croner: 9.1.0 crossws: 0.3.5 - db0: 0.3.4 + db0: 0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)) defu: 6.1.4 destr: 2.0.5 dot-prop: 10.1.0 @@ -8056,7 +8443,7 @@ snapshots: unenv: 2.0.0-rc.23 unimport: 5.5.0 unplugin-utils: 0.3.1 - unstorage: 1.17.1(db0@0.3.4)(ioredis@5.8.2) + unstorage: 1.17.1(db0@0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)))(ioredis@5.8.2) untyped: 2.0.0 unwasm: 0.3.11 youch: 4.1.0-beta.11 @@ -8128,16 +8515,16 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1): + nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)))(drizzle-orm@0.44.7(postgres@3.4.7))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1): dependencies: '@dxup/nuxt': 0.2.0(magicast@0.5.0) '@nuxt/cli': 3.29.3(magicast@0.5.0) '@nuxt/devtools': 2.7.0(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) '@nuxt/kit': 4.2.0(magicast@0.5.0) - '@nuxt/nitro-server': 4.2.0(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.3) + '@nuxt/nitro-server': 4.2.0(db0@0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)))(drizzle-orm@0.44.7(postgres@3.4.7))(ioredis@5.8.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)))(drizzle-orm@0.44.7(postgres@3.4.7))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.3) '@nuxt/schema': 4.2.0 '@nuxt/telemetry': 2.6.6(magicast@0.5.0) - '@nuxt/vite-builder': 4.2.0(@types/node@22.18.13)(eslint@9.38.0(jiti@2.6.1))(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1) + '@nuxt/vite-builder': 4.2.0(@types/node@22.18.13)(eslint@9.38.0(jiti@2.6.1))(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@22.18.13)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)))(drizzle-orm@0.44.7(postgres@3.4.7))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1) '@unhead/vue': 2.0.19(vue@3.5.22(typescript@5.9.3)) '@vue/shared': 3.5.22 c12: 3.3.1(magicast@0.5.0) @@ -8698,6 +9085,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres@3.4.7: {} + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -9367,7 +9756,7 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unstorage@1.17.1(db0@0.3.4)(ioredis@5.8.2): + unstorage@1.17.1(db0@0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)))(ioredis@5.8.2): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -9378,7 +9767,7 @@ snapshots: ofetch: 1.5.0 ufo: 1.6.1 optionalDependencies: - db0: 0.3.4 + db0: 0.3.4(drizzle-orm@0.44.7(postgres@3.4.7)) ioredis: 5.8.2 untun@0.1.3: diff --git a/server/database/migrations/0000_tiresome_malcolm_colcord.sql b/server/database/migrations/0000_tiresome_malcolm_colcord.sql new file mode 100644 index 0000000..6b702d1 --- /dev/null +++ b/server/database/migrations/0000_tiresome_malcolm_colcord.sql @@ -0,0 +1,86 @@ +CREATE TYPE "public"."order_status" AS ENUM('pending', 'paid', 'processing', 'completed', 'failed');--> statement-breakpoint +CREATE TYPE "public"."salutation" AS ENUM('male', 'female', 'other');--> statement-breakpoint +CREATE TABLE "cart_items" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "cart_id" uuid NOT NULL, + "product_id" uuid NOT NULL, + "quantity" integer DEFAULT 1 NOT NULL, + "added_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "carts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid, + "session_id" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "order_items" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "order_id" uuid NOT NULL, + "product_id" uuid NOT NULL, + "product_snapshot" jsonb NOT NULL, + "quantity" integer DEFAULT 1 NOT NULL, + "price_snapshot" numeric(10, 2) NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "orders" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "order_number" text NOT NULL, + "user_id" uuid NOT NULL, + "total_amount" numeric(10, 2) NOT NULL, + "status" "order_status" DEFAULT 'pending' NOT NULL, + "billing_address" jsonb NOT NULL, + "payment_id" text, + "payment_completed_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "orders_order_number_unique" UNIQUE("order_number") +); +--> statement-breakpoint +CREATE TABLE "products" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "nav_product_id" text NOT NULL, + "name" text NOT NULL, + "description" text NOT NULL, + "price" numeric(10, 2) NOT NULL, + "stock_quantity" integer DEFAULT 0 NOT NULL, + "category" text NOT NULL, + "active" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "products_nav_product_id_unique" UNIQUE("nav_product_id") +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "experimenta_id" text NOT NULL, + "email" text NOT NULL, + "first_name" text NOT NULL, + "last_name" text NOT NULL, + "salutation" "salutation", + "date_of_birth" timestamp, + "phone" text, + "street" text, + "post_code" text, + "city" text, + "country_code" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "users_experimenta_id_unique" UNIQUE("experimenta_id") +); +--> statement-breakpoint +ALTER TABLE "cart_items" ADD CONSTRAINT "cart_items_cart_id_carts_id_fk" FOREIGN KEY ("cart_id") REFERENCES "public"."carts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cart_items" ADD CONSTRAINT "cart_items_product_id_products_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."products"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "carts" ADD CONSTRAINT "carts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "order_items" ADD CONSTRAINT "order_items_order_id_orders_id_fk" FOREIGN KEY ("order_id") REFERENCES "public"."orders"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "order_items" ADD CONSTRAINT "order_items_product_id_products_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."products"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "orders" ADD CONSTRAINT "orders_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "orders_order_number_idx" ON "orders" USING btree ("order_number");--> statement-breakpoint +CREATE INDEX "orders_user_id_idx" ON "orders" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "orders_status_idx" ON "orders" USING btree ("status");--> statement-breakpoint +CREATE INDEX "products_nav_product_id_idx" ON "products" USING btree ("nav_product_id");--> statement-breakpoint +CREATE INDEX "products_active_idx" ON "products" USING btree ("active");--> statement-breakpoint +CREATE INDEX "products_category_idx" ON "products" USING btree ("category"); \ No newline at end of file diff --git a/server/database/migrations/meta/0000_snapshot.json b/server/database/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..6cc836e --- /dev/null +++ b/server/database/migrations/meta/0000_snapshot.json @@ -0,0 +1,645 @@ +{ + "id": "edf5f532-2f87-4dfd-8478-0861f6542e29", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.cart_items": { + "name": "cart_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "cart_id": { + "name": "cart_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "added_at": { + "name": "added_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cart_items_cart_id_carts_id_fk": { + "name": "cart_items_cart_id_carts_id_fk", + "tableFrom": "cart_items", + "tableTo": "carts", + "columnsFrom": [ + "cart_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cart_items_product_id_products_id_fk": { + "name": "cart_items_product_id_products_id_fk", + "tableFrom": "cart_items", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.carts": { + "name": "carts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "carts_user_id_users_id_fk": { + "name": "carts_user_id_users_id_fk", + "tableFrom": "carts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.order_items": { + "name": "order_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "product_snapshot": { + "name": "product_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "price_snapshot": { + "name": "price_snapshot", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "order_items_order_id_orders_id_fk": { + "name": "order_items_order_id_orders_id_fk", + "tableFrom": "order_items", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "order_items_product_id_products_id_fk": { + "name": "order_items_product_id_products_id_fk", + "tableFrom": "order_items", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orders": { + "name": "orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_number": { + "name": "order_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_amount": { + "name": "total_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "order_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "billing_address": { + "name": "billing_address", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payment_id": { + "name": "payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_completed_at": { + "name": "payment_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "orders_order_number_idx": { + "name": "orders_order_number_idx", + "columns": [ + { + "expression": "order_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "orders_user_id_idx": { + "name": "orders_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "orders_status_idx": { + "name": "orders_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "orders_user_id_users_id_fk": { + "name": "orders_user_id_users_id_fk", + "tableFrom": "orders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "orders_order_number_unique": { + "name": "orders_order_number_unique", + "nullsNotDistinct": false, + "columns": [ + "order_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.products": { + "name": "products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "nav_product_id": { + "name": "nav_product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "stock_quantity": { + "name": "stock_quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "products_nav_product_id_idx": { + "name": "products_nav_product_id_idx", + "columns": [ + { + "expression": "nav_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "products_active_idx": { + "name": "products_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "products_category_idx": { + "name": "products_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "products_nav_product_id_unique": { + "name": "products_nav_product_id_unique", + "nullsNotDistinct": false, + "columns": [ + "nav_product_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "experimenta_id": { + "name": "experimenta_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "salutation": { + "name": "salutation", + "type": "salutation", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "date_of_birth": { + "name": "date_of_birth", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "street": { + "name": "street", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "post_code": { + "name": "post_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_experimenta_id_unique": { + "name": "users_experimenta_id_unique", + "nullsNotDistinct": false, + "columns": [ + "experimenta_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.order_status": { + "name": "order_status", + "schema": "public", + "values": [ + "pending", + "paid", + "processing", + "completed", + "failed" + ] + }, + "public.salutation": { + "name": "salutation", + "schema": "public", + "values": [ + "male", + "female", + "other" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/server/database/migrations/meta/_journal.json b/server/database/migrations/meta/_journal.json new file mode 100644 index 0000000..4fb93cd --- /dev/null +++ b/server/database/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1761820599588, + "tag": "0000_tiresome_malcolm_colcord", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/server/database/schema.ts b/server/database/schema.ts new file mode 100644 index 0000000..793644c --- /dev/null +++ b/server/database/schema.ts @@ -0,0 +1,278 @@ +/** + * Database Schema for my.experimenta.science + * + * This schema defines the core data model for the experimenta e-commerce platform. + * It manages user profiles, products, shopping carts, and orders. + * + * Key Design Decisions: + * + * 1. UUID Primary Keys: + * - We use UUIDs instead of serial/auto-increment IDs for all primary keys + * - Benefits: + * * Globally unique across distributed systems (no coordination needed) + * * Can be generated client-side without database round-trip + * * More secure - prevents enumeration attacks (guessing IDs) + * * Future-proof for horizontal scaling and multi-region deployment + * - Trade-off: Slightly larger index size (16 bytes vs 4/8 bytes for integers) + * - Implementation: Using PostgreSQL's gen_random_uuid() via defaultRandom() + * + * 2. JSONB for Flexible Data: + * - billing_address (orders table): Stores address snapshot at time of purchase + * - product_snapshot (order_items table): Stores immutable product details + * - Why JSONB instead of normalized tables: + * * Immutability: Order data should never change, even if user updates profile + * * Flexibility: Can store varying address formats without schema changes + * * Performance: Avoids complex joins for order history queries + * * Simplicity: Reduces number of tables and foreign key constraints + * - PostgreSQL JSONB provides efficient indexing and query capabilities if needed + * + * 3. Decimal Types for Money: + * - All price and amount fields use decimal(10, 2) - exact precision with 2 decimal places + * - Why not float/double: + * * Floating-point arithmetic has rounding errors (0.1 + 0.2 ≠ 0.3) + * * Financial calculations require exact precision for legal/tax compliance + * * Example: €19.99 * 3 = €59.97 exactly (not €59.970000000001) + * - precision: 10 allows up to €99,999,999.99 (sufficient for individual orders) + * - Note: When sending to X-API/NAV ERP, convert to cents (integer) to avoid precision loss + * + * 4. Timestamps for Audit Trail: + * - Every table has created_at (when record was created) + * - Most tables have updated_at (last modification time) + * - Benefits: + * * Debugging: Track when issues occurred + * * Analytics: Understand user behavior patterns + * * Compliance: Required for financial record-keeping + * * Data integrity: Detect stale or corrupted data + * - Implementation: Using PostgreSQL's timestamp with timezone (timestamptz) + * + * 5. Foreign Key Constraints: + * - onDelete: 'cascade' - Deleting parent removes children (e.g., cart → cart_items) + * - onDelete: 'restrict' - Prevent deletion if children exist (e.g., user with orders) + * - Ensures referential integrity at database level (not just application logic) + * + * 6. Indexes for Query Performance: + * - Strategic indexes on frequently queried columns: + * * products: nav_product_id, active, category (ERP sync, filtering) + * * orders: order_number (lookup), user_id (user history), status (admin filtering) + * - Trade-off: Faster reads, slightly slower writes (acceptable for e-commerce) + * + * 7. Enums for Type Safety: + * - PostgreSQL enums for status fields (salutation, order_status) + * - Benefits: + * * Database-level validation (prevents invalid values) + * * Type safety in TypeScript (via Drizzle inference) + * * Self-documenting code (clear set of valid values) + * - Note: Adding new enum values requires migration (ALTER TYPE) + */ + +import { relations } from 'drizzle-orm' +import { + boolean, + decimal, + index, + integer, + jsonb, + pgEnum, + pgTable, + text, + timestamp, + uuid, +} from 'drizzle-orm/pg-core' + +/** + * Enums + */ + +// User salutation options +export const salutationEnum = pgEnum('salutation', ['male', 'female', 'other']) + +// Order status lifecycle +export const orderStatusEnum = pgEnum('order_status', [ + 'pending', + 'paid', + 'processing', + 'completed', + 'failed', +]) + +/** + * Users Table + * Stores local user profiles linked to Cidaas authentication + */ +export const users = pgTable('users', { + id: uuid('id').primaryKey().defaultRandom(), + experimentaId: text('experimenta_id').unique().notNull(), // Cidaas sub (user ID) + email: text('email').notNull(), + firstName: text('first_name').notNull(), + lastName: text('last_name').notNull(), + + // Optional profile fields + salutation: salutationEnum('salutation'), + dateOfBirth: timestamp('date_of_birth', { mode: 'date' }), + phone: text('phone'), + + // Billing address fields (optional, filled during checkout or profile edit) + street: text('street'), + postCode: text('post_code'), + city: text('city'), + countryCode: text('country_code'), // ISO 3166-1 alpha-2 (e.g., 'DE', 'AT') + + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}) + +/** + * Products Table + * Synced from NAV ERP via push API + */ +export const products = pgTable( + 'products', + { + id: uuid('id').primaryKey().defaultRandom(), + navProductId: text('nav_product_id').unique().notNull(), // NAV ERP product identifier + name: text('name').notNull(), + description: text('description').notNull(), + price: decimal('price', { precision: 10, scale: 2 }).notNull(), // EUR with 2 decimal places + stockQuantity: integer('stock_quantity').notNull().default(0), + category: text('category').notNull(), // e.g., 'makerspace-annual-pass' + active: boolean('active').notNull().default(true), // Whether product is available for purchase + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), + }, + (table) => ({ + navProductIdIdx: index('products_nav_product_id_idx').on(table.navProductId), + activeIdx: index('products_active_idx').on(table.active), + categoryIdx: index('products_category_idx').on(table.category), + }) +) + +/** + * Carts Table + * Shopping carts for both authenticated and guest users + */ +export const carts = pgTable('carts', { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }), // null for guest carts + sessionId: text('session_id').notNull(), // Browser session identifier for guest carts + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}) + +/** + * Cart Items Table + * Individual line items in a shopping cart + */ +export const cartItems = pgTable('cart_items', { + id: uuid('id').primaryKey().defaultRandom(), + cartId: uuid('cart_id') + .notNull() + .references(() => carts.id, { onDelete: 'cascade' }), + productId: uuid('product_id') + .notNull() + .references(() => products.id, { onDelete: 'restrict' }), // Don't allow deleting products with cart items + quantity: integer('quantity').notNull().default(1), + addedAt: timestamp('added_at').defaultNow().notNull(), +}) + +/** + * Orders Table + * Completed orders submitted to NAV ERP via X-API + */ +export const orders = pgTable( + 'orders', + { + id: uuid('id').primaryKey().defaultRandom(), + orderNumber: text('order_number').unique().notNull(), // Human-readable order number (e.g., 'EXP-2024-001234') + userId: uuid('user_id') + .notNull() + .references(() => users.id, { onDelete: 'restrict' }), // Don't allow deleting users with orders + totalAmount: decimal('total_amount', { precision: 10, scale: 2 }).notNull(), // EUR with 2 decimal places + status: orderStatusEnum('status').notNull().default('pending'), + + // Billing address snapshot at time of order + billingAddress: jsonb('billing_address').notNull(), // { street, postCode, city, countryCode, salutation, firstName, lastName } + + // Payment information + paymentId: text('payment_id'), // PayPal transaction ID + paymentCompletedAt: timestamp('payment_completed_at'), + + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), + }, + (table) => ({ + orderNumberIdx: index('orders_order_number_idx').on(table.orderNumber), + userIdIdx: index('orders_user_id_idx').on(table.userId), + statusIdx: index('orders_status_idx').on(table.status), + }) +) + +/** + * Order Items Table + * Individual line items in an order with product snapshot + */ +export const orderItems = pgTable('order_items', { + id: uuid('id').primaryKey().defaultRandom(), + orderId: uuid('order_id') + .notNull() + .references(() => orders.id, { onDelete: 'cascade' }), + productId: uuid('product_id') + .notNull() + .references(() => products.id, { onDelete: 'restrict' }), // Don't allow deleting products with order items + productSnapshot: jsonb('product_snapshot').notNull(), // Immutable product details at time of purchase { name, description, navProductId } + quantity: integer('quantity').notNull().default(1), + priceSnapshot: decimal('price_snapshot', { precision: 10, scale: 2 }).notNull(), // Price at time of purchase + createdAt: timestamp('created_at').defaultNow().notNull(), +}) + +/** + * Relations + * Define table relationships for Drizzle's relational query API + */ + +export const usersRelations = relations(users, ({ many }) => ({ + carts: many(carts), + orders: many(orders), +})) + +export const cartsRelations = relations(carts, ({ one, many }) => ({ + user: one(users, { + fields: [carts.userId], + references: [users.id], + }), + items: many(cartItems), +})) + +export const cartItemsRelations = relations(cartItems, ({ one }) => ({ + cart: one(carts, { + fields: [cartItems.cartId], + references: [carts.id], + }), + product: one(products, { + fields: [cartItems.productId], + references: [products.id], + }), +})) + +export const ordersRelations = relations(orders, ({ one, many }) => ({ + user: one(users, { + fields: [orders.userId], + references: [users.id], + }), + items: many(orderItems), +})) + +export const orderItemsRelations = relations(orderItems, ({ one }) => ({ + order: one(orders, { + fields: [orderItems.orderId], + references: [orders.id], + }), + product: one(products, { + fields: [orderItems.productId], + references: [products.id], + }), +})) + +export const productsRelations = relations(products, ({ many }) => ({ + cartItems: many(cartItems), + orderItems: many(orderItems), +}))