diff --git a/fix-constraint.ts b/fix-constraint.ts new file mode 100644 index 0000000..89c6760 --- /dev/null +++ b/fix-constraint.ts @@ -0,0 +1,43 @@ +import { drizzle } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; +import { sql } from 'drizzle-orm'; + +async function fixConstraint() { + const dbUrl = process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/tenantpilot'; + console.log('🔌 Connecting to:', dbUrl.replace(/:[^:@]+@/, ':****@')); + + const pool = new Pool({ connectionString: dbUrl }); + const db = drizzle(pool); + + try { + console.log('⏳ Removing duplicate rows...'); + await db.execute(sql` + DELETE FROM policy_settings a + USING policy_settings b + WHERE a.id > b.id + AND a.tenant_id = b.tenant_id + AND a.graph_policy_id = b.graph_policy_id + AND a.setting_name = b.setting_name + `); + + console.log('⏳ Dropping old index...'); + await db.execute(sql`DROP INDEX IF EXISTS policy_settings_upsert_idx`); + + console.log('⏳ Adding unique constraint...'); + await db.execute(sql` + ALTER TABLE policy_settings + ADD CONSTRAINT policy_settings_upsert_unique + UNIQUE(tenant_id, graph_policy_id, setting_name) + `); + + console.log('✅ Constraint added successfully!'); + await pool.end(); + process.exit(0); + } catch (error) { + console.error('❌ Failed:', error); + await pool.end(); + process.exit(1); + } +} + +fixConstraint(); diff --git a/lib/db/migrations/0001_reflective_dormammu.sql b/lib/db/migrations/0001_reflective_dormammu.sql new file mode 100644 index 0000000..10bd098 --- /dev/null +++ b/lib/db/migrations/0001_reflective_dormammu.sql @@ -0,0 +1,3 @@ +DROP INDEX "policy_settings_upsert_idx";--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "tenant_id" text;--> statement-breakpoint +ALTER TABLE "policy_settings" ADD CONSTRAINT "policy_settings_upsert_unique" UNIQUE("tenant_id","graph_policy_id","setting_name"); \ No newline at end of file diff --git a/lib/db/migrations/meta/0001_snapshot.json b/lib/db/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..3416ae7 --- /dev/null +++ b/lib/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,450 @@ +{ + "id": "65f6746a-011c-4712-97db-c41f4b3e6547", + "prevId": "7bea20d0-987b-4a12-8446-a5966f2eb3e8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_settings": { + "name": "policy_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "policy_name": { + "name": "policy_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "policy_type": { + "name": "policy_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "setting_name": { + "name": "setting_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "setting_value": { + "name": "setting_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "graph_policy_id": { + "name": "graph_policy_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "policy_settings_tenant_id_idx": { + "name": "policy_settings_tenant_id_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "policy_settings_setting_name_idx": { + "name": "policy_settings_setting_name_idx", + "columns": [ + { + "expression": "setting_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "policy_settings_upsert_unique": { + "name": "policy_settings_upsert_unique", + "nullsNotDistinct": false, + "columns": [ + "tenant_id", + "graph_policy_id", + "setting_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "stripe_current_period_end": { + "name": "stripe_current_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "subscriptions_user_id_user_id_fk": { + "name": "subscriptions_user_id_user_id_fk", + "tableFrom": "subscriptions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "subscriptions_user_id_stripe_customer_id_pk": { + "name": "subscriptions_user_id_stripe_customer_id_pk", + "columns": [ + "user_id", + "stripe_customer_id" + ] + } + }, + "uniqueConstraints": { + "subscriptions_user_id_unique": { + "name": "subscriptions_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "subscriptions_stripe_customer_id_unique": { + "name": "subscriptions_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + }, + "subscriptions_stripe_subscription_id_unique": { + "name": "subscriptions_stripe_subscription_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_subscription_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json index b72a88b..66a9180 100644 --- a/lib/db/migrations/meta/_journal.json +++ b/lib/db/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1764967548076, "tag": "0000_tiny_skin", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1765060303141, + "tag": "0001_reflective_dormammu", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/db/schema/policySettings.ts b/lib/db/schema/policySettings.ts index cedabd2..1426821 100644 --- a/lib/db/schema/policySettings.ts +++ b/lib/db/schema/policySettings.ts @@ -1,4 +1,4 @@ -import { pgTable, text, timestamp, index } from 'drizzle-orm/pg-core'; +import { pgTable, text, timestamp, index, unique } from 'drizzle-orm/pg-core'; import { createId } from '@paralleldrive/cuid2'; export const POLICY_TYPES = [ @@ -43,7 +43,8 @@ export const policySettings = pgTable( settingNameIdx: index('policy_settings_setting_name_idx').on( table.settingName ), - upsertIdx: index('policy_settings_upsert_idx').on( + // Unique constraint for ON CONFLICT upsert + upsertUnique: unique('policy_settings_upsert_unique').on( table.tenantId, table.graphPolicyId, table.settingName