Browse Source

Add Drizzle ORM setup and database configuration

- Add drizzle.config.ts for database migrations
- Create server/database/ structure with schema
- Add drizzle-orm and postgres dependencies
- Add db:* npm scripts for database management
main
Bastian Masanek 2 months ago
parent
commit
ef9845c5c5
  1. 10
      drizzle.config.ts
  2. 9
      package.json
  3. 421
      pnpm-lock.yaml
  4. 86
      server/database/migrations/0000_tiresome_malcolm_colcord.sql
  5. 645
      server/database/migrations/meta/0000_snapshot.json
  6. 13
      server/database/migrations/meta/_journal.json
  7. 278
      server/database/schema.ts

10
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!,
},
})

9
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",

421
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:

86
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");

645
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": {}
}
}

13
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
}
]
}

278
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),
}))
Loading…
Cancel
Save