From 6e5af360cae02e781b584973ec393ccb7fa38731 Mon Sep 17 00:00:00 2001 From: Touha <39002455+iamtouha@users.noreply.github.com> Date: Sat, 6 Apr 2024 16:52:52 +0600 Subject: [PATCH] Feat/migrate to postgres (#52) * remove old migrations * Update database configuration to use PostgreSQL * Update database configuration to use PostgreSQL * Update database connection and migration to use PostgreSQL * Update database URL and remove unused code * Update lucia Drizzle adapter to use PostgreSQL * Update DATABASE_PREFIX constant * Update database description in landing page * Update database name in README --- .env.example | 7 +- .github/workflows/check.yaml | 2 +- README.md | 6 +- drizzle.config.ts | 4 +- drizzle/0000_lovely_vivisector.sql | 63 ----- drizzle/meta/0000_snapshot.json | 405 ----------------------------- drizzle/meta/_journal.json | 13 - package.json | 13 +- pnpm-lock.yaml | 192 +++++++------- src/app/(landing)/page.tsx | 2 +- src/env.js | 6 +- src/lib/auth/index.ts | 4 +- src/lib/constants.ts | 4 +- src/server/db/index.ts | 10 +- src/server/db/migrate.ts | 12 +- src/server/db/schema.ts | 49 ++-- 16 files changed, 162 insertions(+), 630 deletions(-) delete mode 100644 drizzle/0000_lovely_vivisector.sql delete mode 100644 drizzle/meta/0000_snapshot.json delete mode 100644 drizzle/meta/_journal.json diff --git a/.env.example b/.env.example index f7ffa25..bc00baa 100644 --- a/.env.example +++ b/.env.example @@ -9,10 +9,9 @@ # When adding additional environment variables, the schema in "/src/env.js" # should be updated accordingly. -# Drizzle -# Get the Database URL from the "prisma" dropdown selector in PlanetScale. -# Change the query params at the end of the URL to "?ssl={"rejectUnauthorized":true}" -DATABASE_URL='mysql://YOUR_MYSQL_URL_HERE?ssl={"rejectUnauthorized":true}' +# Drizzle +DATABASE_URL='postgres://YOUR_DATABASE_URL_HERE' + SMTP_HOST='smtp.example-host.com' SMTP_PORT=25 SMTP_USER='smtp_example_username' diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 50533ae..c047044 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -26,7 +26,7 @@ jobs: run: pnpm run typecheck && pnpm run lint env: # use dummy env variables to bypass t3-env check - DATABASE_URL: mysql://test:xxxx@xxxxxxxxx:3306/test + DATABASE_URL: postgres://test:xxxx@xxxxxxxxx:3306/test SMTP_HOST: host SMTP_PORT: 587 SMTP_USER: user diff --git a/README.md b/README.md index dbdfc16..0fbd611 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Lucia is less opinionated than NextAuth, offering greater flexibility for custom - **Lucia + tRPC:** 🔄 Similar to NextAuth with tRPC, granting access to sessions and user information through tRPC procedures. - **Stripe Payment:** 💳 Setup user subscriptions seamlessly with stripe. - **Email template with react-email:** ✉️ Craft your email templates using React. -- **MySQL Database:** 🛢️ Utilize a MySQL database (Planetscale) set up using Drizzle for enhanced performance and type safety. +- **PostgreSQL Database:** 🛢️ Utilize a PostgreSQL database set up using Drizzle for enhanced performance and type safety. - **Database Migration:** 🚀 Included migration script to extend the database schema according to your project needs. ## Tech Stack @@ -26,7 +26,7 @@ Lucia is less opinionated than NextAuth, offering greater flexibility for custom - [Lucia](https://lucia-auth.com/) - [tRPC](https://trpc.io) - [Drizzle ORM](https://orm.drizzle.team/) -- [Planetscale](https://planetscale.com/) +- [PostgreSQL](https://www.postgresql.org/) - [Stripe](https://stripe.com/) - [Tailwind CSS](https://tailwindcss.com) - [Shadcn UI](https://ui.shadcn.com/) @@ -47,7 +47,7 @@ Lucia is less opinionated than NextAuth, offering greater flexibility for custom - [ ] Update Password - [x] Stripe Integration -- [x] API Rate-Limiting see branch - [upstash-ratelimiting](https://github.com/iamtouha/next-lucia-auth/tree/upstash-ratelimiting) + - [ ] Admin Dashboard (under consideration) - [ ] Role-Based Access Policy (under consideration) diff --git a/drizzle.config.ts b/drizzle.config.ts index 0ebf478..7f17ced 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -4,9 +4,9 @@ import { DATABASE_PREFIX } from "@/lib/constants"; export default defineConfig({ schema: "./src/server/db/schema.ts", out: "./drizzle", - driver: "mysql2", + driver: "pg", dbCredentials: { - uri: process.env.DATABASE_URL!, + connectionString: process.env.DATABASE_URL!, }, tablesFilter: [`${DATABASE_PREFIX}_*`], }); diff --git a/drizzle/0000_lovely_vivisector.sql b/drizzle/0000_lovely_vivisector.sql deleted file mode 100644 index 02b2e01..0000000 --- a/drizzle/0000_lovely_vivisector.sql +++ /dev/null @@ -1,63 +0,0 @@ -CREATE TABLE `acme_v3_email_verification_codes` ( - `id` int AUTO_INCREMENT NOT NULL, - `user_id` varchar(21) NOT NULL, - `email` varchar(255) NOT NULL, - `code` varchar(8) NOT NULL, - `expires_at` datetime NOT NULL, - CONSTRAINT `acme_v3_email_verification_codes_id` PRIMARY KEY(`id`), - CONSTRAINT `acme_v3_email_verification_codes_user_id_unique` UNIQUE(`user_id`) -); ---> statement-breakpoint -CREATE TABLE `acme_v3_password_reset_tokens` ( - `id` varchar(40) NOT NULL, - `user_id` varchar(21) NOT NULL, - `expires_at` datetime NOT NULL, - CONSTRAINT `acme_v3_password_reset_tokens_id` PRIMARY KEY(`id`) -); ---> statement-breakpoint -CREATE TABLE `acme_v3_posts` ( - `id` varchar(15) NOT NULL, - `user_id` varchar(255) NOT NULL, - `title` varchar(255) NOT NULL, - `excerpt` varchar(255) NOT NULL, - `content` text NOT NULL, - `status` varchar(10) NOT NULL DEFAULT 'draft', - `tags` varchar(255), - `created_at` timestamp NOT NULL DEFAULT (now()), - `updated_at` timestamp ON UPDATE CURRENT_TIMESTAMP, - CONSTRAINT `acme_v3_posts_id` PRIMARY KEY(`id`) -); ---> statement-breakpoint -CREATE TABLE `acme_v3_sessions` ( - `id` varchar(255) NOT NULL, - `user_id` varchar(21) NOT NULL, - `expires_at` datetime NOT NULL, - CONSTRAINT `acme_v3_sessions_id` PRIMARY KEY(`id`) -); ---> statement-breakpoint -CREATE TABLE `acme_v3_users` ( - `id` varchar(21) NOT NULL, - `discord_id` varchar(255), - `email` varchar(255) NOT NULL, - `email_verified` boolean NOT NULL DEFAULT false, - `hashed_password` varchar(255), - `avatar` varchar(255), - `stripe_subscription_id` varchar(191), - `stripe_price_id` varchar(191), - `stripe_customer_id` varchar(191), - `stripe_current_period_end` timestamp, - `created_at` timestamp NOT NULL DEFAULT (now()), - `updated_at` timestamp ON UPDATE CURRENT_TIMESTAMP, - CONSTRAINT `acme_v3_users_id` PRIMARY KEY(`id`), - CONSTRAINT `acme_v3_users_discord_id_unique` UNIQUE(`discord_id`), - CONSTRAINT `acme_v3_users_email_unique` UNIQUE(`email`) -); ---> statement-breakpoint -CREATE INDEX `user_idx` ON `acme_v3_email_verification_codes` (`user_id`);--> statement-breakpoint -CREATE INDEX `email_idx` ON `acme_v3_email_verification_codes` (`email`);--> statement-breakpoint -CREATE INDEX `user_idx` ON `acme_v3_password_reset_tokens` (`user_id`);--> statement-breakpoint -CREATE INDEX `user_idx` ON `acme_v3_posts` (`user_id`);--> statement-breakpoint -CREATE INDEX `post_created_at_idx` ON `acme_v3_posts` (`created_at`);--> statement-breakpoint -CREATE INDEX `user_idx` ON `acme_v3_sessions` (`user_id`);--> statement-breakpoint -CREATE INDEX `email_idx` ON `acme_v3_users` (`email`);--> statement-breakpoint -CREATE INDEX `discord_idx` ON `acme_v3_users` (`discord_id`); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json deleted file mode 100644 index 39ac8eb..0000000 --- a/drizzle/meta/0000_snapshot.json +++ /dev/null @@ -1,405 +0,0 @@ -{ - "version": "5", - "dialect": "mysql", - "id": "45c5b9ad-6f11-4d15-9abb-43bcbc25afab", - "prevId": "00000000-0000-0000-0000-000000000000", - "tables": { - "acme_v3_email_verification_codes": { - "name": "acme_v3_email_verification_codes", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "user_id": { - "name": "user_id", - "type": "varchar(21)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "code": { - "name": "code", - "type": "varchar(8)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "expires_at": { - "name": "expires_at", - "type": "datetime", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "user_idx": { - "name": "user_idx", - "columns": [ - "user_id" - ], - "isUnique": false - }, - "email_idx": { - "name": "email_idx", - "columns": [ - "email" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": { - "acme_v3_email_verification_codes_id": { - "name": "acme_v3_email_verification_codes_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": { - "acme_v3_email_verification_codes_user_id_unique": { - "name": "acme_v3_email_verification_codes_user_id_unique", - "columns": [ - "user_id" - ] - } - } - }, - "acme_v3_password_reset_tokens": { - "name": "acme_v3_password_reset_tokens", - "columns": { - "id": { - "name": "id", - "type": "varchar(40)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "varchar(21)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "expires_at": { - "name": "expires_at", - "type": "datetime", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "user_idx": { - "name": "user_idx", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": { - "acme_v3_password_reset_tokens_id": { - "name": "acme_v3_password_reset_tokens_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {} - }, - "acme_v3_posts": { - "name": "acme_v3_posts", - "columns": { - "id": { - "name": "id", - "type": "varchar(15)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "title": { - "name": "title", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "excerpt": { - "name": "excerpt", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "varchar(10)", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'draft'" - }, - "tags": { - "name": "tags", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "onUpdate": true - } - }, - "indexes": { - "user_idx": { - "name": "user_idx", - "columns": [ - "user_id" - ], - "isUnique": false - }, - "post_created_at_idx": { - "name": "post_created_at_idx", - "columns": [ - "created_at" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": { - "acme_v3_posts_id": { - "name": "acme_v3_posts_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {} - }, - "acme_v3_sessions": { - "name": "acme_v3_sessions", - "columns": { - "id": { - "name": "id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "varchar(21)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "expires_at": { - "name": "expires_at", - "type": "datetime", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "user_idx": { - "name": "user_idx", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": { - "acme_v3_sessions_id": { - "name": "acme_v3_sessions_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {} - }, - "acme_v3_users": { - "name": "acme_v3_users", - "columns": { - "id": { - "name": "id", - "type": "varchar(21)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "discord_id": { - "name": "discord_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email_verified": { - "name": "email_verified", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "hashed_password": { - "name": "hashed_password", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "avatar": { - "name": "avatar", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "stripe_subscription_id": { - "name": "stripe_subscription_id", - "type": "varchar(191)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "stripe_price_id": { - "name": "stripe_price_id", - "type": "varchar(191)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "stripe_customer_id": { - "name": "stripe_customer_id", - "type": "varchar(191)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "stripe_current_period_end": { - "name": "stripe_current_period_end", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "onUpdate": true - } - }, - "indexes": { - "email_idx": { - "name": "email_idx", - "columns": [ - "email" - ], - "isUnique": false - }, - "discord_idx": { - "name": "discord_idx", - "columns": [ - "discord_id" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": { - "acme_v3_users_id": { - "name": "acme_v3_users_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": { - "acme_v3_users_discord_id_unique": { - "name": "acme_v3_users_discord_id_unique", - "columns": [ - "discord_id" - ] - }, - "acme_v3_users_email_unique": { - "name": "acme_v3_users_email_unique", - "columns": [ - "email" - ] - } - } - } - }, - "schemas": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - } -} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json deleted file mode 100644 index f13c066..0000000 --- a/drizzle/meta/_journal.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "5", - "dialect": "mysql", - "entries": [ - { - "idx": 0, - "version": "5", - "when": 1707040027391, - "tag": "0000_lovely_vivisector", - "breakpoints": true - } - ] -} \ No newline at end of file diff --git a/package.json b/package.json index 916cc1a..7372048 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "build": "next build", - "db:push": "dotenv drizzle-kit push:mysql", - "db:generate": "dotenv drizzle-kit generate:mysql", + "db:push": "dotenv drizzle-kit push:pg", + "db:generate": "dotenv drizzle-kit generate:pg", "db:migrate": "dotenv tsx src/server/db/migrate.ts", "db:studio": "dotenv drizzle-kit studio", "dev": "next dev", @@ -18,7 +18,6 @@ "dependencies": { "@hookform/resolvers": "^3.3.2", "@lucia-auth/adapter-drizzle": "1.0.0", - "@planetscale/database": "^1.11.0", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -37,13 +36,13 @@ "arctic": "^1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", - "mysql2": "^3.7.1", - "drizzle-orm": "^0.29.3", + "drizzle-orm": "^0.30.7", "lucia": "3.0.0", "next": "^14.1.0", "next-themes": "^0.2.1", "nodemailer": "^6.9.9", "oslo": "^1.0.1", + "postgres": "^3.4.4", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.49.0", @@ -73,9 +72,9 @@ "@typescript-eslint/parser": "^6.11.0", "autoprefixer": "^10.4.14", "dotenv-cli": "^7.3.0", - "drizzle-kit": "^0.20.13", + "drizzle-kit": "^0.20.14", "eslint": "^8.54.0", - "mysql2": "^3.7.1", + "pg": "^8.11.5", "postcss": "^8.4.31", "prettier": "^3.1.0", "prettier-plugin-tailwindcss": "^0.5.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f4f3ce..3cdc215 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,6 @@ dependencies: '@lucia-auth/adapter-drizzle': specifier: 1.0.0 version: 1.0.0(lucia@3.0.0) - '@planetscale/database': - specifier: ^1.11.0 - version: 1.11.0 '@radix-ui/react-alert-dialog': specifier: ^1.0.5 version: 1.0.5(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) @@ -69,14 +66,11 @@ dependencies: specifier: ^2.0.0 version: 2.0.0 drizzle-orm: - specifier: ^0.29.3 - version: 0.29.4(@planetscale/database@1.11.0)(@types/react@18.2.43)(mysql2@3.9.1)(react@18.2.0) + specifier: ^0.30.7 + version: 0.30.7(@types/react@18.2.43)(pg@8.11.5)(postgres@3.4.4)(react@18.2.0) lucia: specifier: 3.0.0 version: 3.0.0 - mysql2: - specifier: ^3.7.1 - version: 3.9.1 next: specifier: ^14.1.0 version: 14.1.0(react-dom@18.2.0)(react@18.2.0) @@ -89,6 +83,9 @@ dependencies: oslo: specifier: ^1.0.1 version: 1.0.1 + postgres: + specifier: ^3.4.4 + version: 3.4.4 react: specifier: 18.2.0 version: 18.2.0 @@ -173,11 +170,14 @@ devDependencies: specifier: ^7.3.0 version: 7.3.0 drizzle-kit: - specifier: ^0.20.13 + specifier: ^0.20.14 version: 0.20.14 eslint: specifier: ^8.54.0 version: 8.55.0 + pg: + specifier: ^8.11.5 + version: 8.11.5 postcss: specifier: ^8.4.31 version: 8.4.32 @@ -1218,11 +1218,6 @@ packages: dev: false optional: true - /@planetscale/database@1.11.0: - resolution: {integrity: sha512-aWbU+D/IRHoDE9975y+Q4c+EwwAWxCPwFId+N1AhQVFXzbeJMkj6KN2iQtoi03elcLMRdfT+V3i9Z4WRw+/oIA==} - engines: {node: '>=16'} - dev: false - /@radix-ui/primitive@1.0.1: resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} dependencies: @@ -2738,11 +2733,6 @@ packages: has-property-descriptors: 1.0.1 dev: false - /denque@2.1.0: - resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} - engines: {node: '>=0.10'} - dev: false - /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2860,20 +2850,23 @@ packages: - supports-color dev: true - /drizzle-orm@0.29.4(@planetscale/database@1.11.0)(@types/react@18.2.43)(mysql2@3.9.1)(react@18.2.0): - resolution: {integrity: sha512-ZnSM8TAxFhzH7p1s3+w3pRE/eKaOeNkH9SKitm717pubDVVcV2I0BCDBPGKV+pe02+wMfw37ntlTcCyo2rA3IA==} + /drizzle-orm@0.30.7(@types/react@18.2.43)(pg@8.11.5)(postgres@3.4.4)(react@18.2.0): + resolution: {integrity: sha512-9qefSZQlu2fO2qv24piHyWFWcxcOY15//0v4j8qomMqaxzipNoG+fUBrQ7Ftk7PY7APRbRdn/nkEXWxiI4a8mw==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=3' + '@electric-sql/pglite': '>=0.1.1' '@libsql/client': '*' '@neondatabase/serverless': '>=0.1' + '@op-engineering/op-sqlite': '>=2' '@opentelemetry/api': ^1.4.1 '@planetscale/database': '>=1' '@types/better-sqlite3': '*' '@types/pg': '*' '@types/react': '>=18' '@types/sql.js': '*' - '@vercel/postgres': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' better-sqlite3: '>=7' bun-types: '*' expo-sqlite: '>=13.2.0' @@ -2890,10 +2883,14 @@ packages: optional: true '@cloudflare/workers-types': optional: true + '@electric-sql/pglite': + optional: true '@libsql/client': optional: true '@neondatabase/serverless': optional: true + '@op-engineering/op-sqlite': + optional: true '@opentelemetry/api': optional: true '@planetscale/database': @@ -2908,6 +2905,8 @@ packages: optional: true '@vercel/postgres': optional: true + '@xata.io/client': + optional: true better-sqlite3: optional: true bun-types: @@ -2931,9 +2930,9 @@ packages: sqlite3: optional: true dependencies: - '@planetscale/database': 1.11.0 '@types/react': 18.2.43 - mysql2: 3.9.1 + pg: 8.11.5 + postgres: 3.4.4 react: 18.2.0 dev: false @@ -3318,12 +3317,6 @@ packages: /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - /generate-function@2.3.1: - resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} - dependencies: - is-property: 1.0.2 - dev: false - /get-intrinsic@1.2.3: resolution: {integrity: sha512-JIcZczvcMVE7AUOP+X72bh8HqHBRxFdz5PDHYtNG/lE3yk9b3KZBJlwFcTyPYjg3L4RLLmZJzvjxhaZVapxFrQ==} engines: {node: '>= 0.4'} @@ -3617,13 +3610,6 @@ packages: entities: 4.5.0 dev: false - /iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: false - /ignore@5.3.0: resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} engines: {node: '>= 4'} @@ -3756,10 +3742,6 @@ packages: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} dev: true - /is-property@1.0.2: - resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} - dev: false - /is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} @@ -3887,10 +3869,6 @@ packages: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} dev: true - /long@5.2.3: - resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} - dev: false - /longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} dev: false @@ -3920,16 +3898,6 @@ packages: dependencies: yallist: 4.0.0 - /lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - dev: false - - /lru-cache@8.0.5: - resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==} - engines: {node: '>=16.14'} - dev: false - /lru-queue@0.1.0: resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} dependencies: @@ -4445,20 +4413,6 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - /mysql2@3.9.1: - resolution: {integrity: sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==} - engines: {node: '>= 8.0'} - dependencies: - denque: 2.1.0 - generate-function: 2.3.1 - iconv-lite: 0.6.3 - long: 5.2.3 - lru-cache: 8.0.5 - named-placeholders: 1.1.3 - seq-queue: 0.0.5 - sqlstring: 2.3.3 - dev: false - /mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} dependencies: @@ -4466,13 +4420,6 @@ packages: object-assign: 4.1.1 thenify-all: 1.6.0 - /named-placeholders@1.1.3: - resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} - engines: {node: '>=12.0.0'} - dependencies: - lru-cache: 7.18.3 - dev: false - /nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -4690,6 +4637,60 @@ packages: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} dev: false + /pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + requiresBuild: true + optional: true + + /pg-connection-string@2.6.4: + resolution: {integrity: sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==} + + /pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + /pg-pool@3.6.2(pg@8.11.5): + resolution: {integrity: sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==} + peerDependencies: + pg: '>=8.0' + dependencies: + pg: 8.11.5 + + /pg-protocol@1.6.1: + resolution: {integrity: sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==} + + /pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + /pg@8.11.5: + resolution: {integrity: sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + dependencies: + pg-connection-string: 2.6.4 + pg-pool: 3.6.2(pg@8.11.5) + pg-protocol: 1.6.1 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + + /pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + dependencies: + split2: 4.2.0 + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -4785,6 +4786,29 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + /postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + /postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + /postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + dependencies: + xtend: 4.0.2 + + /postgres@3.4.4: + resolution: {integrity: sha512-IbyN+9KslkqcXa8AO9fxpk97PA4pzewvpi2B3Dwy9u4zpV32QicaEdgmF3eSQUzdRk7ttDHQejNgAEr4XoeH4A==} + engines: {node: '>=12'} + dev: false + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -5123,10 +5147,6 @@ packages: dependencies: queue-microtask: 1.2.3 - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: false - /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: @@ -5146,10 +5166,6 @@ packages: dependencies: lru-cache: 6.0.0 - /seq-queue@0.0.5: - resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} - dev: false - /server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} dev: false @@ -5231,10 +5247,9 @@ packages: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} dev: false - /sqlstring@2.3.3: - resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} - engines: {node: '>= 0.6'} - dev: false + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} /streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} @@ -5666,7 +5681,6 @@ packages: /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - dev: false /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} diff --git a/src/app/(landing)/page.tsx b/src/app/(landing)/page.tsx index aa099d8..90844d1 100644 --- a/src/app/(landing)/page.tsx +++ b/src/app/(landing)/page.tsx @@ -44,7 +44,7 @@ const features = [ }, { name: "Database", - description: "Drizzle with planetscale mysql database", + description: "Drizzle with postgres database", logo: Drizzle, }, { diff --git a/src/env.js b/src/env.js index 0223a31..98ba91f 100644 --- a/src/env.js +++ b/src/env.js @@ -11,12 +11,10 @@ export const env = createEnv({ .string() .url() .refine( - (str) => !str.includes("YOUR_MYSQL_URL_HERE"), + (str) => !str.includes("YOUR_DATABASE_URL_HERE"), "You forgot to change the default URL", ), - NODE_ENV: z - .enum(["development", "test", "production"]) - .default("development"), + NODE_ENV: z.enum(["development", "test", "production"]).default("development"), DISCORD_CLIENT_ID: z.string().trim().min(1), DISCORD_CLIENT_SECRET: z.string().trim().min(1), SMTP_HOST: z.string().trim().min(1), diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index bca56e0..80c1b71 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -1,6 +1,6 @@ import { Lucia, TimeSpan } from "lucia"; import { Discord } from "arctic"; -import { DrizzleMySQLAdapter } from "@lucia-auth/adapter-drizzle"; +import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle"; import { env } from "@/env.js"; import { db } from "@/server/db"; import { sessions, users, type User as DbUser } from "@/server/db/schema"; @@ -9,7 +9,7 @@ import { sessions, users, type User as DbUser } from "@/server/db/schema"; // import { webcrypto } from "node:crypto"; // globalThis.crypto = webcrypto as Crypto; -const adapter = new DrizzleMySQLAdapter(db, sessions, users); +const adapter = new DrizzlePostgreSQLAdapter(db, sessions, users); export const lucia = new Lucia(adapter, { getSessionAttributes: (/* attributes */) => { diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 0ac96e4..c3e4879 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,5 +1,5 @@ export const APP_TITLE = "Acme"; -export const DATABASE_PREFIX = "acme_v3"; +export const DATABASE_PREFIX = "acme"; export const EMAIL_SENDER = '"Acme" '; export const redirects = { @@ -9,4 +9,4 @@ export const redirects = { afterLogout: "/", toVerify: "/verify-email", afterVerify: "/dashboard", -} as const; \ No newline at end of file +} as const; diff --git a/src/server/db/index.ts b/src/server/db/index.ts index edee0d9..10ce938 100644 --- a/src/server/db/index.ts +++ b/src/server/db/index.ts @@ -1,9 +1,11 @@ -import { drizzle } from "drizzle-orm/planetscale-serverless"; -import { Client } from "@planetscale/database"; - +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; import { env } from "@/env"; import * as schema from "./schema"; -export const connection = new Client({ url: env.DATABASE_URL }); +export const connection = postgres(env.DATABASE_URL, { + max_lifetime: 10, // Remove this line if you're deploying to Docker / VPS + // idle_timeout: 20, // Uncomment this line if you're deploying to Docker / VPS +}); export const db = drizzle(connection, { schema }); diff --git a/src/server/db/migrate.ts b/src/server/db/migrate.ts index 385ad03..a5e0279 100644 --- a/src/server/db/migrate.ts +++ b/src/server/db/migrate.ts @@ -1,13 +1,13 @@ -import mysql from "mysql2/promise"; -import { drizzle } from "drizzle-orm/mysql2"; -import { migrate } from "drizzle-orm/mysql2/migrator"; +import postgres from "postgres"; +import { drizzle } from "drizzle-orm/postgres-js"; +import { migrate } from "drizzle-orm/postgres-js/migrator"; import { env } from "@/env"; import * as schema from "./schema"; export async function runMigrate() { - const connection = await mysql.createConnection(env.DATABASE_URL); - const db = drizzle(connection, { schema, mode: "planetscale" }); + const connection = postgres(env.DATABASE_URL); + const db = drizzle(connection, { schema }); console.log("⏳ Running migrations..."); @@ -15,6 +15,8 @@ export async function runMigrate() { await migrate(db, { migrationsFolder: "drizzle" }); + await connection.end(); + const end = Date.now(); console.log(`✅ Migrations completed in ${end - start}ms`); diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 8eb3197..8574060 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -1,19 +1,18 @@ -import { mysqlTableCreator } from "drizzle-orm/mysql-core"; -import { DATABASE_PREFIX as prefix } from "@/lib/constants"; +import { relations } from "drizzle-orm"; import { + pgTableCreator, + serial, boolean, - datetime, index, - int, text, timestamp, varchar, -} from "drizzle-orm/mysql-core"; -import { relations } from "drizzle-orm"; +} from "drizzle-orm/pg-core"; +import { DATABASE_PREFIX as prefix } from "@/lib/constants"; -export const mysqlTable = mysqlTableCreator((name) => `${prefix}_${name}`); +export const pgTable = pgTableCreator((name) => `${prefix}_${name}`); -export const users = mysqlTable( +export const users = pgTable( "users", { id: varchar("id", { length: 21 }).primaryKey(), @@ -27,57 +26,57 @@ export const users = mysqlTable( stripeCustomerId: varchar("stripe_customer_id", { length: 191 }), stripeCurrentPeriodEnd: timestamp("stripe_current_period_end"), createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at", { mode: "date" }).$onUpdate(() => new Date()), }, (t) => ({ - emailIdx: index("email_idx").on(t.email), - discordIdx: index("discord_idx").on(t.discordId), + emailIdx: index("user_email_idx").on(t.email), + discordIdx: index("user_discord_idx").on(t.discordId), }), ); export type User = typeof users.$inferSelect; export type NewUser = typeof users.$inferInsert; -export const sessions = mysqlTable( +export const sessions = pgTable( "sessions", { id: varchar("id", { length: 255 }).primaryKey(), userId: varchar("user_id", { length: 21 }).notNull(), - expiresAt: datetime("expires_at").notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date" }).notNull(), }, (t) => ({ - userIdx: index("user_idx").on(t.userId), + userIdx: index("session_user_idx").on(t.userId), }), ); -export const emailVerificationCodes = mysqlTable( +export const emailVerificationCodes = pgTable( "email_verification_codes", { - id: int("id").primaryKey().autoincrement(), + id: serial("id").primaryKey(), userId: varchar("user_id", { length: 21 }).unique().notNull(), email: varchar("email", { length: 255 }).notNull(), code: varchar("code", { length: 8 }).notNull(), - expiresAt: datetime("expires_at").notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date" }).notNull(), }, (t) => ({ - userIdx: index("user_idx").on(t.userId), - emailIdx: index("email_idx").on(t.email), + userIdx: index("verification_code_user_idx").on(t.userId), + emailIdx: index("verification_code_email_idx").on(t.email), }), ); -export const passwordResetTokens = mysqlTable( +export const passwordResetTokens = pgTable( "password_reset_tokens", { id: varchar("id", { length: 40 }).primaryKey(), userId: varchar("user_id", { length: 21 }).notNull(), - expiresAt: datetime("expires_at").notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date" }).notNull(), }, (t) => ({ - userIdx: index("user_idx").on(t.userId), + userIdx: index("password_token_user_idx").on(t.userId), }), ); -export const posts = mysqlTable( +export const posts = pgTable( "posts", { id: varchar("id", { length: 15 }).primaryKey(), @@ -90,10 +89,10 @@ export const posts = mysqlTable( .notNull(), tags: varchar("tags", { length: 255 }), createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at", { mode: "date" }).$onUpdate(() => new Date()), }, (t) => ({ - userIdx: index("user_idx").on(t.userId), + userIdx: index("post_user_idx").on(t.userId), createdAtIdx: index("post_created_at_idx").on(t.createdAt), }), );