From ef3593ba637039c7cd3dea127888b0925fa4448c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 09:13:27 +0000 Subject: [PATCH 1/2] fix(auth): migrate user IDs to Clerk IDs and resolve connection errors - Updated lib/db/schema.ts to use text for users.id and all referencing foreign keys. - Removed the redundant clerk_user_id column from the users table. - Simplified lib/auth/get-current-user.ts to use Clerk ID as the internal primary key. - Updated the Clerk webhook (app/api/clerk/webhook/route.ts) to match the new ID logic. - Hardened lib/db/index.ts and lib/db/migrate.ts against invalid DATABASE_URL placeholders. - Generated migration 0003_complex_sir_ram.sql to apply schema changes. This resolves the "Error resolving Clerk user to DB user" by aligning the codebase with the intended architectural guideline and fixing the connection string parsing crash. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- app/api/clerk/webhook/route.ts | 22 +- drizzle/migrations/0003_complex_sir_ram.sql | 10 + drizzle/migrations/meta/0003_snapshot.json | 762 ++++++++++++++++++++ drizzle/migrations/meta/_journal.json | 7 + lib/auth/get-current-user.ts | 32 +- lib/db/index.ts | 11 +- lib/db/migrate.ts | 19 +- lib/db/schema.ts | 19 +- 8 files changed, 827 insertions(+), 55 deletions(-) create mode 100644 drizzle/migrations/0003_complex_sir_ram.sql create mode 100644 drizzle/migrations/meta/0003_snapshot.json diff --git a/app/api/clerk/webhook/route.ts b/app/api/clerk/webhook/route.ts index 5228c2fb..05092cf1 100644 --- a/app/api/clerk/webhook/route.ts +++ b/app/api/clerk/webhook/route.ts @@ -50,39 +50,40 @@ export async function POST(req: Request) { } // Do something with the payload - // For this guide, you can log the payload to your console const { id } = evt.data const eventType = evt.type if (eventType === 'user.created' || eventType === 'user.updated') { - const { id, email_addresses, first_name, last_name } = evt.data as any + const { id, email_addresses } = evt.data as any const email = email_addresses[0]?.email_address if (id) { - // Upsert user + // Upsert user using Clerk ID as the primary key 'id' const [existingUser] = await db.select() .from(users) - .where(eq(users.clerkUserId, id)) + .where(eq(users.id, id)) .limit(1); if (existingUser) { await db.update(users) .set({ email }) - .where(eq(users.clerkUserId, id)); + .where(eq(users.id, id)); } else { - // Check by email first to link + // Check by email first (in case of migration or previous Supabase user) const [existingEmailUser] = await db.select() .from(users) .where(eq(users.email, email)) .limit(1); if (existingEmailUser) { - await db.update(users) - .set({ clerkUserId: id }) - .where(eq(users.id, existingEmailUser.id)); + // If found by email but ID is different, we have a conflict. + // For a clean transition, we could update the ID, but primary keys are stable. + // In this simplified model, we'll just insert if not exact ID match. + // Or we update the email to null on the old user if we want to "take over" the email. + await db.update(users).set({ id: id }).where(eq(users.id, existingEmailUser.id)); } else { await db.insert(users).values({ - clerkUserId: id, + id: id, email, role: 'viewer' }); @@ -93,7 +94,6 @@ export async function POST(req: Request) { if (eventType === 'user.deleted') { // Optionally handle user deletion - // For safety, we might not want to delete user data entirely } return new Response('', { status: 200 }) diff --git a/drizzle/migrations/0003_complex_sir_ram.sql b/drizzle/migrations/0003_complex_sir_ram.sql new file mode 100644 index 00000000..dcdc3892 --- /dev/null +++ b/drizzle/migrations/0003_complex_sir_ram.sql @@ -0,0 +1,10 @@ +ALTER TABLE "calendar_notes" ALTER COLUMN "user_id" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "chat_participants" ALTER COLUMN "user_id" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "chats" ALTER COLUMN "user_id" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "locations" ALTER COLUMN "user_id" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "messages" ALTER COLUMN "user_id" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "prompt_generation_jobs" ALTER COLUMN "user_id" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "system_prompts" ALTER COLUMN "user_id" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "users" ALTER COLUMN "id" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "users" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint +ALTER TABLE "visualizations" ALTER COLUMN "user_id" SET DATA TYPE text; \ No newline at end of file diff --git a/drizzle/migrations/meta/0003_snapshot.json b/drizzle/migrations/meta/0003_snapshot.json new file mode 100644 index 00000000..57e345d1 --- /dev/null +++ b/drizzle/migrations/meta/0003_snapshot.json @@ -0,0 +1,762 @@ +{ + "id": "51cf73db-ea21-43c4-8541-aa61eb2c428d", + "prevId": "fd89f5aa-13b0-4dd4-9f50-4e921eea0f4f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.calendar_notes": { + "name": "calendar_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_tags": { + "name": "location_tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "user_tags": { + "name": "user_tags", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "map_feature_id": { + "name": "map_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "calendar_notes_user_id_users_id_fk": { + "name": "calendar_notes_user_id_users_id_fk", + "tableFrom": "calendar_notes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "calendar_notes_chat_id_chats_id_fk": { + "name": "calendar_notes_chat_id_chats_id_fk", + "tableFrom": "calendar_notes", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_participants": { + "name": "chat_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'collaborator'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_participants_chat_id_chats_id_fk": { + "name": "chat_participants_chat_id_chats_id_fk", + "tableFrom": "chat_participants", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_participants_user_id_users_id_fk": { + "name": "chat_participants_user_id_users_id_fk", + "tableFrom": "chat_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_participants_chat_user_unique": { + "name": "chat_participants_chat_user_unique", + "nullsNotDistinct": false, + "columns": [ + "chat_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chats": { + "name": "chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Untitled Chat'" + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'private'" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_path": { + "name": "share_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shareable_link_id": { + "name": "shareable_link_id", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "chats_user_id_users_id_fk": { + "name": "chats_user_id_users_id_fk", + "tableFrom": "chats", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chats_shareable_link_id_unique": { + "name": "chats_shareable_link_id_unique", + "nullsNotDistinct": false, + "columns": [ + "shareable_link_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.locations": { + "name": "locations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "geojson": { + "name": "geojson", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "geometry": { + "name": "geometry", + "type": "geometry(GEOMETRY, 4326)", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "locations_user_id_users_id_fk": { + "name": "locations_user_id_users_id_fk", + "tableFrom": "locations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "locations_chat_id_chats_id_fk": { + "name": "locations_chat_id_chats_id_fk", + "tableFrom": "locations", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "messages_chat_id_chats_id_fk": { + "name": "messages_chat_id_chats_id_fk", + "tableFrom": "messages", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_user_id_users_id_fk": { + "name": "messages_user_id_users_id_fk", + "tableFrom": "messages", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_location_id_locations_id_fk": { + "name": "messages_location_id_locations_id_fk", + "tableFrom": "messages", + "tableTo": "locations", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.prompt_generation_jobs": { + "name": "prompt_generation_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result_prompt": { + "name": "result_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "prompt_generation_jobs_user_id_users_id_fk": { + "name": "prompt_generation_jobs_user_id_users_id_fk", + "tableFrom": "prompt_generation_jobs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_prompts": { + "name": "system_prompts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "system_prompts_user_id_users_id_fk": { + "name": "system_prompts_user_id_users_id_fk", + "tableFrom": "system_prompts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'viewer'" + }, + "selected_model": { + "name": "selected_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.visualizations": { + "name": "visualizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'map_layer'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "geometry": { + "name": "geometry", + "type": "geometry(GEOMETRY, 4326)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "visualizations_user_id_users_id_fk": { + "name": "visualizations_user_id_users_id_fk", + "tableFrom": "visualizations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "visualizations_chat_id_chats_id_fk": { + "name": "visualizations_chat_id_chats_id_fk", + "tableFrom": "visualizations", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 259f488a..42e1bb34 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1782395315062, "tag": "0002_lively_black_widow", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1783501051316, + "tag": "0003_complex_sir_ram", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/auth/get-current-user.ts b/lib/auth/get-current-user.ts index 5ab5385f..893c12cb 100644 --- a/lib/auth/get-current-user.ts +++ b/lib/auth/get-current-user.ts @@ -22,23 +22,22 @@ export async function getClerkUserIdOnServer(): Promise { } /** - * Resolves a Clerk User ID to our internal database UUID. + * Resolves a Clerk User ID to our internal database ID. * If the user doesn't exist in our DB yet, it will create one. * * @param clerkUserId The Clerk user ID to resolve - * @returns {Promise} Our internal database UUID for the user + * @returns {Promise} Our internal database ID for the user */ export async function resolveClerkUserToDbUser(clerkUserId: string): Promise { if (AUTH_DISABLED_FLAG && clerkUserId === MOCK_USER_ID) { - // Return a consistent UUID for the mock user - return '00000000-0000-0000-0000-000000000000'; + return MOCK_USER_ID; } try { - // 1. Try to find the user by clerkUserId + // 1. Try to find the user by ID (which IS the clerkUserId) const [existingUser] = await db.select({ id: users.id }) .from(users) - .where(eq(users.clerkUserId, clerkUserId)) + .where(eq(users.id, clerkUserId)) .limit(1); if (existingUser) { @@ -51,27 +50,23 @@ export async function resolveClerkUserToDbUser(clerkUserId: string): Promise} The internal user UUID if authenticated, otherwise null. + * @returns {Promise} The internal user ID if authenticated, otherwise null. */ export async function getCurrentUserIdOnServer(): Promise { const clerkUserId = await getClerkUserIdOnServer(); diff --git a/lib/db/index.ts b/lib/db/index.ts index f0b43eb7..09a25945 100644 --- a/lib/db/index.ts +++ b/lib/db/index.ts @@ -1,5 +1,5 @@ import { drizzle } from 'drizzle-orm/node-postgres'; -import { Pool, type PoolConfig } from 'pg'; // Uses Pool from pg, import PoolConfig +import { Pool, type PoolConfig } from 'pg'; import * as dotenv from 'dotenv'; import * as schema from './schema'; @@ -7,16 +7,19 @@ dotenv.config({ path: '.env.local' }); // In production/build environments, we might not have DATABASE_URL immediately available // especially during Next.js static optimization phases. -if (!process.env.DATABASE_URL && process.env.NODE_ENV === 'production') { +const dbUrl = process.env.DATABASE_URL; +if (!dbUrl && process.env.NODE_ENV === 'production') { console.warn('DATABASE_URL environment variable is not set. Database features will be unavailable.'); } const poolConfig: PoolConfig = { - connectionString: process.env.DATABASE_URL || 'postgres://localhost:5432/postgres', + connectionString: dbUrl && dbUrl !== "postgresql://user:password@host:port/db" + ? dbUrl + : 'postgres://localhost:5432/postgres', }; // Conditionally apply SSL for Supabase URLs -if (process.env.DATABASE_URL && process.env.DATABASE_URL.includes('supabase.co')) { +if (dbUrl && dbUrl.includes('supabase.co')) { poolConfig.ssl = { rejectUnauthorized: false, }; diff --git a/lib/db/migrate.ts b/lib/db/migrate.ts index 7c696a4d..5e7338f8 100644 --- a/lib/db/migrate.ts +++ b/lib/db/migrate.ts @@ -6,30 +6,28 @@ import * as dotenv from 'dotenv'; dotenv.config({ path: '.env.local' }); async function runMigrations() { - if (!process.env.DATABASE_URL) { - throw new Error('DATABASE_URL environment variable is not set for migrations'); + const dbUrl = process.env.DATABASE_URL; + if (!dbUrl || dbUrl === "postgresql://user:password@host:port/db") { + console.warn('Skipping migrations: DATABASE_URL is not set or is placeholder.'); + return; } const pool = new Pool({ - connectionString: process.env.DATABASE_URL, - ssl: { - rejectUnauthorized: false, // Ensure this is appropriate for your Supabase connection - }, - // max: 1, // Optional: restrict to 1 connection for migration + connectionString: dbUrl, + ssl: dbUrl.includes('supabase.co') ? { rejectUnauthorized: false } : undefined, }); const db = drizzle(pool); console.log('Running database migrations...'); try { - // Point to the directory containing your migration files await migrate(db, { migrationsFolder: './drizzle/migrations' }); console.log('Migrations completed successfully.'); } catch (error) { console.error('Error running migrations:', error); - process.exit(1); // Exit with error code + process.exit(1); } finally { - await pool.end(); // Ensure the connection pool is closed + await pool.end(); } } @@ -37,5 +35,4 @@ if (process.env.EXECUTE_MIGRATIONS === 'true') { runMigrations(); } else { console.log('Skipping migrations. Set EXECUTE_MIGRATIONS=true to run them.'); - console.log('To run migrations, use the "npm run db:migrate" or "bun run db:migrate" script, which sets this variable.'); } diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 0b41dceb..84eec0f0 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -20,17 +20,16 @@ const vector = customType<{ data: number[] }>({ }); export const users = pgTable('users', { - id: uuid('id').primaryKey().defaultRandom(), + id: text('id').primaryKey(), // Clerk user ID as primary key email: text('email').unique(), // Enforced unique for user identity role: text('role').default('viewer'), selectedModel: text('selected_model'), systemPrompt: text('system_prompt'), - clerkUserId: text('clerk_user_id').unique(), }); export const chats = pgTable('chats', { id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), title: text('title').notNull().default('Untitled Chat'), visibility: text('visibility').default('private'), path: text('path'), @@ -42,7 +41,7 @@ export const chats = pgTable('chats', { export const locations = pgTable('locations', { id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), chatId: uuid('chat_id').references(() => chats.id, { onDelete: 'cascade' }), geojson: jsonb('geojson').notNull(), geometry: geometry('geometry'), @@ -53,7 +52,7 @@ export const locations = pgTable('locations', { export const messages = pgTable('messages', { id: uuid('id').primaryKey().defaultRandom(), chatId: uuid('chat_id').notNull().references(() => chats.id, { onDelete: 'cascade' }), - userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), role: text('role').notNull(), content: text('content').notNull(), embedding: vector('embedding'), @@ -64,7 +63,7 @@ export const messages = pgTable('messages', { export const chatParticipants = pgTable('chat_participants', { id: uuid('id').primaryKey().defaultRandom(), chatId: uuid('chat_id').notNull().references(() => chats.id, { onDelete: 'cascade' }), - userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), role: text('role').notNull().default('collaborator'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), }, (t) => ({ @@ -74,7 +73,7 @@ export const chatParticipants = pgTable('chat_participants', { export const systemPrompts = pgTable('system_prompts', { id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), prompt: text('prompt').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), @@ -82,7 +81,7 @@ export const systemPrompts = pgTable('system_prompts', { export const visualizations = pgTable('visualizations', { id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), chatId: uuid('chat_id').references(() => chats.id, { onDelete: 'cascade' }), type: text('type').notNull().default('map_layer'), data: jsonb('data').notNull(), @@ -92,7 +91,7 @@ export const visualizations = pgTable('visualizations', { export const calendarNotes = pgTable('calendar_notes', { id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), chatId: uuid('chat_id').references(() => chats.id, { onDelete: 'cascade' }), date: timestamp('date', { withTimezone: true }).notNull(), content: text('content').notNull(), @@ -185,7 +184,7 @@ export const visualizationsRelations = relations(visualizations, ({ one }) => ({ export const promptGenerationJobs = pgTable('prompt_generation_jobs', { id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), domain: text('domain').notNull(), status: text('status').notNull().default('pending'), // pending | processing | complete | error resultPrompt: text('result_prompt'), From 88faf547075de5e782a697a6770833e8f090757e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:58:33 +0000 Subject: [PATCH 2/2] fix(auth): fix build error and ensure manual ID for user inserts - Fixed TypeScript error in lib/actions/users.ts by providing a UUID for the id field. - Verified all other db.insert(users) calls include the id field. - Maintained consistency with the new schema where users.id is text and has no default. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- lib/actions/users.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/actions/users.ts b/lib/actions/users.ts index 6fba7f8b..77790b1e 100644 --- a/lib/actions/users.ts +++ b/lib/actions/users.ts @@ -5,6 +5,7 @@ import { db } from '@/lib/db'; import { users } from '@/lib/db/schema'; import { eq, ilike } from 'drizzle-orm'; import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user'; +import { v4 as uuidv4 } from 'uuid'; export type UserRole = "admin" | "editor" | "viewer"; @@ -75,6 +76,7 @@ export async function addUser(newUser: { email: string; role: UserRole }): Promi } const [insertedUser] = await db.insert(users).values({ + id: uuidv4(), // Manual ID generation since db default is gone email: newUser.email, role: newUser.role, }).returning({