Repository navigation
Staging - #32
Staging#32
Conversation
when opensaas API routes (mounted directly on express) throw 500s, the error response was missing CORS headers. browsers reported this as a CORS error instead of showing the real 500. now the catch block reflects the request origin so errors are visible in devtools.
creates calls, contacts, workspace_subscriptions, workspace_usage tables that were referenced in code but never had CREATE TABLE migrations. includes combined-all.sql for running all 18 migrations in dependency order (pgvector skipped — not available on railway).
5 files were using raw fetch() without Bearer token, causing 401s on every opensaas API route even when logged in. switched to authenticatedFetch which reads the JWT from the cookie store.
Review Summary by QodoFix CORS headers, add missing migrations, and use authenticatedFetch for opensaas APIs
WalkthroughsDescription• Add CORS headers to error responses in opensaas routes • Create missing database migrations for calls, contacts, workspace subscriptions • Replace raw fetch() with authenticatedFetch in 5 frontend files • Ensure JWT authentication on all opensaas API calls Diagramflowchart LR
A["Frontend Files"] -->|"Replace fetch with authenticatedFetch"| B["authenticatedFetch utility"]
B -->|"Includes JWT from cookie"| C["Backend API Routes"]
D["Express Error Handler"] -->|"Add CORS headers on 500"| E["Browser receives proper error"]
F["Missing Migrations"] -->|"Create tables"| G["calls, contacts, workspace_subscriptions, workspace_usage"]
File Changes1. packages/twenty-front/src/modules/assistant/commands/handlers.ts
|
Code Review by Qodo
1. combined-all.sql missing down
|
| BEGIN; | ||
|
|
||
| -- === 001-create-files.sql === | ||
| -- DEV-744: File storage backend — files + file_attachments tables | ||
| -- Phase 6.1: S3-backed file storage with workspace scoping | ||
|
|
||
| CREATE TABLE IF NOT EXISTS files ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id UUID NOT NULL, | ||
| name VARCHAR(255) NOT NULL, | ||
| mime_type VARCHAR(100) NOT NULL, | ||
| size BIGINT NOT NULL, | ||
| storage_key VARCHAR(500) NOT NULL, | ||
| folder VARCHAR(255), | ||
| tags TEXT[], | ||
| uploaded_by UUID, | ||
| created_at TIMESTAMPTZ DEFAULT NOW(), | ||
| updated_at TIMESTAMPTZ DEFAULT NOW() | ||
| ); | ||
|
|
||
| CREATE INDEX idx_files_workspace ON files(workspace_id); | ||
| CREATE INDEX idx_files_folder ON files(workspace_id, folder); | ||
| CREATE INDEX idx_files_mime_type ON files(mime_type); | ||
|
|
||
| CREATE TABLE IF NOT EXISTS file_attachments ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| file_id UUID NOT NULL REFERENCES files(id) ON DELETE CASCADE, | ||
| entity_type VARCHAR(50) NOT NULL, | ||
| entity_id UUID NOT NULL, | ||
| created_at TIMESTAMPTZ DEFAULT NOW() | ||
| ); | ||
|
|
||
| CREATE INDEX idx_file_attachments_file ON file_attachments(file_id); | ||
| CREATE INDEX idx_file_attachments_entity ON file_attachments(entity_type, entity_id); | ||
|
|
||
|
|
||
| -- === 004-create-caller-id-lock-tables.sql === | ||
| -- caller ID locks — prevents concurrent use of the same outbound number | ||
| CREATE TABLE IF NOT EXISTS caller_id_locks ( | ||
| phone_number VARCHAR(20) PRIMARY KEY, | ||
| user_id VARCHAR(255) NOT NULL, | ||
| call_sid VARCHAR(64) NOT NULL, | ||
| acquired_at TIMESTAMPTZ DEFAULT NOW(), | ||
| expires_at TIMESTAMPTZ NOT NULL | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_caller_locks_call_sid ON caller_id_locks(call_sid); | ||
| CREATE INDEX IF NOT EXISTS idx_caller_locks_user ON caller_id_locks(user_id); | ||
| CREATE INDEX IF NOT EXISTS idx_caller_locks_expires ON caller_id_locks(expires_at); | ||
|
|
||
| -- area code locations for geo-proximity (local presence) | ||
| CREATE TABLE IF NOT EXISTS area_code_locations ( | ||
| area_code VARCHAR(5) PRIMARY KEY, | ||
| latitude DECIMAL(9,6) NOT NULL, | ||
| longitude DECIMAL(9,6) NOT NULL, | ||
| city VARCHAR(100), | ||
| state VARCHAR(50) | ||
| ); | ||
|
|
||
|
|
||
| -- === 015-create-calls.sql === | ||
| -- calls table: stores call history for analytics, history, and recording lookup | ||
| CREATE TABLE IF NOT EXISTS calls ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id UUID NOT NULL, | ||
| call_sid VARCHAR(64), | ||
| conference_name VARCHAR(128), | ||
| recording_sid VARCHAR(64), | ||
| contact_id UUID, | ||
| direction VARCHAR(16), | ||
| status VARCHAR(32), | ||
| outcome VARCHAR(32), | ||
| "from" VARCHAR(32), | ||
| "to" VARCHAR(32), | ||
| duration_seconds INTEGER DEFAULT 0, | ||
| transcript TEXT, | ||
| analysis JSONB, | ||
| start_time TIMESTAMPTZ, | ||
| end_time TIMESTAMPTZ, | ||
| parallel_group_id VARCHAR(30), | ||
| parallel_position SMALLINT, | ||
| parallel_outcome VARCHAR(20) DEFAULT 'pending', | ||
| parallel_termination_reason VARCHAR(30), | ||
| parallel_terminated_at TIMESTAMPTZ, | ||
| amd_result VARCHAR(20), | ||
| amd_enabled BOOLEAN DEFAULT FALSE, | ||
| created_at TIMESTAMPTZ DEFAULT NOW(), | ||
| updated_at TIMESTAMPTZ DEFAULT NOW() | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_calls_workspace ON calls(workspace_id); | ||
| CREATE INDEX IF NOT EXISTS idx_calls_call_sid ON calls(call_sid); | ||
| CREATE INDEX IF NOT EXISTS idx_calls_start_time ON calls(workspace_id, start_time DESC); | ||
| CREATE INDEX IF NOT EXISTS idx_calls_contact ON calls(contact_id); | ||
| CREATE INDEX IF NOT EXISTS idx_calls_recording ON calls(recording_sid); | ||
| CREATE INDEX IF NOT EXISTS idx_calls_parallel_group ON calls(parallel_group_id); | ||
|
|
||
|
|
||
| -- === 016-create-contacts.sql === | ||
| -- contacts table: local contact records for call history joins and queue management | ||
| CREATE TABLE IF NOT EXISTS contacts ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id UUID NOT NULL, | ||
| name VARCHAR(255), | ||
| phone VARCHAR(32), | ||
| email VARCHAR(255), | ||
| company VARCHAR(255), | ||
| tags TEXT[], | ||
| source VARCHAR(32), | ||
| dnc_status VARCHAR(16) DEFAULT 'allowed', | ||
| address TEXT, | ||
| city VARCHAR(128), | ||
| state VARCHAR(64), | ||
| zip VARCHAR(16), | ||
| created_at TIMESTAMPTZ DEFAULT NOW(), | ||
| updated_at TIMESTAMPTZ DEFAULT NOW() | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_contacts_workspace ON contacts(workspace_id); | ||
| CREATE INDEX IF NOT EXISTS idx_contacts_phone ON contacts(phone); | ||
| CREATE INDEX IF NOT EXISTS idx_contacts_email ON contacts(email); | ||
|
|
||
|
|
||
| -- === 005-create-contact-notes-follow-ups.sql === | ||
| -- contact notes — quick notes linked to contacts | ||
| CREATE TABLE IF NOT EXISTS contact_notes ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| contact_id UUID NOT NULL, | ||
| content TEXT NOT NULL, | ||
| call_id VARCHAR(64), | ||
| created_by VARCHAR(255) NOT NULL, | ||
| workspace_id UUID NOT NULL, | ||
| created_at TIMESTAMPTZ DEFAULT NOW() | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_contact_notes_contact ON contact_notes(contact_id); | ||
|
|
||
| -- contact follow-ups — scheduled follow-up reminders | ||
| CREATE TABLE IF NOT EXISTS contact_follow_ups ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| contact_id UUID NOT NULL, | ||
| scheduled_at TIMESTAMPTZ NOT NULL, | ||
| note TEXT, | ||
| call_id VARCHAR(64), | ||
| status VARCHAR(20) DEFAULT 'pending', | ||
| created_by VARCHAR(255) NOT NULL, | ||
| workspace_id UUID NOT NULL, | ||
| created_at TIMESTAMPTZ DEFAULT NOW(), | ||
| completed_at TIMESTAMPTZ | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_follow_ups_contact ON contact_follow_ups(contact_id); | ||
| CREATE INDEX IF NOT EXISTS idx_follow_ups_scheduled ON contact_follow_ups(scheduled_at) WHERE status = 'pending'; | ||
|
|
||
|
|
||
| -- === 017-create-workspace-subscriptions.sql === | ||
| -- workspace_subscriptions: stripe subscription state per workspace | ||
| CREATE TABLE IF NOT EXISTS workspace_subscriptions ( | ||
| workspace_id UUID PRIMARY KEY, | ||
| stripe_customer_id VARCHAR(255), | ||
| stripe_subscription_id VARCHAR(255), | ||
| status VARCHAR(32) NOT NULL DEFAULT 'inactive', | ||
| plan_name VARCHAR(64), | ||
| interval VARCHAR(16), | ||
| current_period_end TIMESTAMPTZ, | ||
| add_ons JSONB DEFAULT '[]'::jsonb, | ||
| created_at TIMESTAMPTZ DEFAULT NOW(), | ||
| updated_at TIMESTAMPTZ DEFAULT NOW() | ||
| ); | ||
|
|
||
| -- workspace_usage: metered usage tracking per workspace | ||
| CREATE TABLE IF NOT EXISTS workspace_usage ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id UUID NOT NULL, | ||
| metric VARCHAR(64) NOT NULL, | ||
| amount NUMERIC NOT NULL DEFAULT 0, | ||
| period_start TIMESTAMPTZ NOT NULL, | ||
| created_at TIMESTAMPTZ DEFAULT NOW() | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_workspace_usage_lookup ON workspace_usage(workspace_id, metric, period_start); | ||
|
|
||
|
|
||
| -- === 007-create-user-settings.sql === | ||
| -- user_settings: per-user JSONB preferences (notifications, dialer, display, keyboard) | ||
| CREATE TABLE IF NOT EXISTS user_settings ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| user_id VARCHAR(255) NOT NULL, | ||
| workspace_id VARCHAR(255) NOT NULL, | ||
| preferences JSONB NOT NULL DEFAULT '{}', | ||
| created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| UNIQUE (user_id, workspace_id) | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_user_settings_user_workspace | ||
| ON user_settings (user_id, workspace_id); | ||
|
|
||
|
|
||
| -- === 008-create-workspace-settings.sql === | ||
| -- 008-create-workspace-settings.sql | ||
| -- DEV-758: workspace settings — branding, team management, billing | ||
|
|
||
| CREATE TABLE IF NOT EXISTS workspace_settings ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id VARCHAR(255) NOT NULL UNIQUE, | ||
| name VARCHAR(255) NOT NULL DEFAULT '', | ||
| slug VARCHAR(255) NOT NULL DEFAULT '', | ||
| branding JSONB NOT NULL DEFAULT '{}', | ||
| billing_config JSONB NOT NULL DEFAULT '{}', | ||
| created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), | ||
| updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_workspace_settings_workspace_id ON workspace_settings (workspace_id); | ||
|
|
||
| CREATE TABLE IF NOT EXISTS workspace_members ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id VARCHAR(255) NOT NULL, | ||
| user_id UUID, | ||
| email VARCHAR(255) NOT NULL, | ||
| role VARCHAR(20) NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member')), | ||
| status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'active', 'suspended')), | ||
| created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), | ||
| updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), | ||
| last_active_at TIMESTAMP WITH TIME ZONE, | ||
| UNIQUE (workspace_id, email) | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_workspace_members_workspace_id ON workspace_members (workspace_id); | ||
| CREATE INDEX IF NOT EXISTS idx_workspace_members_user_id ON workspace_members (user_id); | ||
|
|
||
|
|
||
| -- === 009-create-ghl-connections.sql === | ||
| -- GHL OAuth connections and sync infrastructure | ||
| CREATE TABLE IF NOT EXISTS ghl_connections ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id UUID NOT NULL UNIQUE, | ||
| location_id VARCHAR(255) NOT NULL, | ||
| access_token_encrypted TEXT NOT NULL, | ||
| refresh_token_encrypted TEXT NOT NULL, | ||
| expires_at TIMESTAMPTZ NOT NULL, | ||
| scopes TEXT[] NOT NULL, | ||
| connected_at TIMESTAMPTZ DEFAULT NOW(), | ||
| last_sync_at TIMESTAMPTZ, | ||
| sync_enabled BOOLEAN DEFAULT true | ||
| ); | ||
|
|
||
| CREATE INDEX idx_ghl_connections_workspace ON ghl_connections (workspace_id); | ||
|
|
||
|
|
||
| -- === 010-create-assistant-conversations.sql === | ||
| -- DEV-811: assistant conversation history + context management | ||
|
|
||
| CREATE TABLE IF NOT EXISTS assistant_conversations ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id UUID NOT NULL, | ||
| user_id UUID NOT NULL, | ||
| summary TEXT, | ||
| created_at TIMESTAMPTZ DEFAULT NOW(), | ||
| updated_at TIMESTAMPTZ DEFAULT NOW() | ||
| ); | ||
|
|
||
| CREATE TABLE IF NOT EXISTS assistant_messages ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| conversation_id UUID NOT NULL REFERENCES assistant_conversations(id) ON DELETE CASCADE, | ||
| role VARCHAR(10) NOT NULL CHECK (role IN ('user', 'assistant')), | ||
| content TEXT NOT NULL, | ||
| commands_executed JSONB, | ||
| created_at TIMESTAMPTZ DEFAULT NOW() | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_assistant_messages_conversation ON assistant_messages(conversation_id, created_at); | ||
| CREATE INDEX IF NOT EXISTS idx_assistant_conversations_user ON assistant_conversations(user_id, updated_at DESC); | ||
| CREATE INDEX IF NOT EXISTS idx_assistant_conversations_workspace ON assistant_conversations(workspace_id); | ||
|
|
||
|
|
||
| -- === 011-create-ghl-pipeline-mappings.sql === | ||
| -- GHL pipeline stage mappings (GHL stages → Twenty stages) | ||
| CREATE TABLE IF NOT EXISTS ghl_pipeline_mappings ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id UUID NOT NULL, | ||
| ghl_pipeline_id VARCHAR(255) NOT NULL, | ||
| ghl_stage_id VARCHAR(255) NOT NULL, | ||
| twenty_pipeline_id UUID NOT NULL, | ||
| twenty_stage_id UUID NOT NULL, | ||
| created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| UNIQUE(workspace_id, ghl_stage_id) | ||
| ); | ||
|
|
||
| CREATE INDEX idx_ghl_pipeline_mappings_workspace ON ghl_pipeline_mappings(workspace_id); | ||
|
|
||
| -- GHL opportunity sync tracking | ||
| CREATE TABLE IF NOT EXISTS ghl_opportunity_sync ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id UUID NOT NULL, | ||
| ghl_opportunity_id VARCHAR(255) NOT NULL, | ||
| ghl_pipeline_id VARCHAR(255) NOT NULL, | ||
| ghl_stage_id VARCHAR(255) NOT NULL, | ||
| twenty_pipeline_id UUID NOT NULL, | ||
| twenty_stage_id UUID NOT NULL, | ||
| ghl_contact_id VARCHAR(255), | ||
| monetary_value NUMERIC(12,2) DEFAULT 0, | ||
| status VARCHAR(50) DEFAULT 'open', | ||
| created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| UNIQUE(workspace_id, ghl_opportunity_id) | ||
| ); | ||
|
|
||
| CREATE INDEX idx_ghl_opportunity_sync_workspace ON ghl_opportunity_sync(workspace_id); | ||
|
|
||
|
|
||
| -- === 013-create-ghl-sync-mappings.sql === | ||
| -- GHL sync mappings table for contact synchronization | ||
| CREATE TABLE IF NOT EXISTS ghl_sync_mappings ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id UUID NOT NULL, | ||
| ghl_contact_id VARCHAR(255) NOT NULL, | ||
| twenty_person_id UUID NOT NULL, | ||
| last_synced_at TIMESTAMPTZ, | ||
| created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| UNIQUE(workspace_id, ghl_contact_id) | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_ghl_sync_mappings_workspace ON ghl_sync_mappings (workspace_id); | ||
| CREATE INDEX IF NOT EXISTS idx_ghl_sync_mappings_ghl_contact ON ghl_sync_mappings (ghl_contact_id); | ||
| CREATE INDEX IF NOT EXISTS idx_ghl_sync_mappings_twenty_person ON ghl_sync_mappings (twenty_person_id); | ||
|
|
||
| -- GHL sync log table for tracking sync operations | ||
| CREATE TABLE IF NOT EXISTS ghl_sync_logs ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id UUID NOT NULL, | ||
| sync_type VARCHAR(50) NOT NULL, -- 'import', 'incremental', 'webhook' | ||
| status VARCHAR(50) NOT NULL, -- 'pending', 'completed', 'failed', 'partial' | ||
| started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| completed_at TIMESTAMPTZ, | ||
| total_contacts INTEGER DEFAULT 0, | ||
| imported_count INTEGER DEFAULT 0, | ||
| updated_count INTEGER DEFAULT 0, | ||
| skipped_count INTEGER DEFAULT 0, | ||
| conflict_count INTEGER DEFAULT 0, | ||
| error_message TEXT, | ||
| details JSONB | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_ghl_sync_logs_workspace ON ghl_sync_logs (workspace_id); | ||
| CREATE INDEX IF NOT EXISTS idx_ghl_sync_logs_started_at ON ghl_sync_logs (started_at DESC); | ||
|
|
||
|
|
||
| -- === 014-ghl-schema-fixes.sql === | ||
| -- GHL webhook events table for idempotency checking | ||
| CREATE TABLE IF NOT EXISTS ghl_webhook_events ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| event_id VARCHAR(255) NOT NULL UNIQUE, | ||
| event_type VARCHAR(100) NOT NULL, | ||
| location_id VARCHAR(255) NOT NULL, | ||
| processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_ghl_webhook_events_location ON ghl_webhook_events (location_id); | ||
|
|
||
| -- Add disconnected_at column to ghl_connections (used by webhook handler) | ||
| ALTER TABLE ghl_connections ADD COLUMN IF NOT EXISTS disconnected_at TIMESTAMPTZ; | ||
|
|
||
|
|
||
| -- === 003-file-attachments-unique.sql === | ||
| -- DEV-749: Add unique constraint to prevent duplicate file attachments | ||
| -- and implement file CRUD (completing the 501 stubs from DEV-744) | ||
|
|
||
| CREATE UNIQUE INDEX IF NOT EXISTS idx_file_attachments_unique | ||
| ON file_attachments (file_id, entity_type, entity_id); | ||
|
|
||
|
|
||
| -- === 012-add-call-history-indexes.sql === | ||
| -- DEV-856: Add indexes for call history queries | ||
|
|
||
| -- For workspace filtering (most queries) | ||
| CREATE INDEX IF NOT EXISTS idx_calls_workspace_start_time ON calls(workspace_id, start_time DESC); | ||
|
|
||
| -- For outcome filtering | ||
| CREATE INDEX IF NOT EXISTS idx_calls_workspace_outcome ON calls(workspace_id, outcome) WHERE outcome IS NOT NULL; | ||
|
|
||
| -- For date range filtering | ||
| CREATE INDEX IF NOT EXISTS idx_calls_workspace_date ON calls(workspace_id, start_time); | ||
|
|
||
| -- For contact_id filtering | ||
| CREATE INDEX IF NOT EXISTS idx_calls_contact ON calls(contact_id) WHERE contact_id IS NOT NULL; | ||
|
|
||
| -- Composite index for history endpoint | ||
| CREATE INDEX IF NOT EXISTS idx_calls_history_query ON calls(workspace_id, outcome, start_time, contact_id); | ||
|
|
||
|
|
||
| -- === 006-create-call-queues.sql === | ||
| CREATE TABLE IF NOT EXISTS call_queues ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id TEXT NOT NULL, | ||
| user_id TEXT NOT NULL, | ||
| name TEXT NOT NULL, | ||
| description TEXT, | ||
| source_type TEXT NOT NULL DEFAULT 'manual', | ||
| source_id TEXT, | ||
| category TEXT NOT NULL DEFAULT 'all', | ||
| calling_mode TEXT NOT NULL DEFAULT 'browser', | ||
| status TEXT NOT NULL DEFAULT 'idle', | ||
| settings JSONB NOT NULL DEFAULT '{}', | ||
| total_contacts INTEGER NOT NULL DEFAULT 0, | ||
| completed_contacts INTEGER NOT NULL DEFAULT 0, | ||
| skipped_contacts INTEGER NOT NULL DEFAULT 0, | ||
| dnc_filtered_count INTEGER NOT NULL DEFAULT 0, | ||
| aggregated_stats JSONB, | ||
| started_at TIMESTAMPTZ, | ||
| completed_at TIMESTAMPTZ, | ||
| created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() | ||
| ); | ||
| CREATE TABLE IF NOT EXISTS queue_items ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| queue_id UUID NOT NULL REFERENCES call_queues(id) ON DELETE CASCADE, | ||
| contact_id TEXT NOT NULL, | ||
| position INTEGER NOT NULL, | ||
| status TEXT NOT NULL DEFAULT 'pending', | ||
| attempts INTEGER NOT NULL DEFAULT 0, | ||
| last_attempt_at TIMESTAMPTZ, | ||
| call_outcome TEXT, | ||
| call_duration_seconds INTEGER, | ||
| skip_reason TEXT, | ||
| notes TEXT, | ||
| created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_queue_items_queue_id ON queue_items(queue_id); | ||
| CREATE INDEX IF NOT EXISTS idx_queue_items_status ON queue_items(status); | ||
| CREATE INDEX IF NOT EXISTS idx_call_queues_user ON call_queues(user_id, workspace_id); | ||
| CREATE INDEX IF NOT EXISTS idx_call_queues_status ON call_queues(status); | ||
| CREATE INDEX IF NOT EXISTS idx_call_queues_workspace ON call_queues(workspace_id); | ||
| CREATE INDEX IF NOT EXISTS idx_queue_items_contact ON queue_items(contact_id); | ||
| CREATE INDEX IF NOT EXISTS idx_queue_items_queue_status ON queue_items(queue_id, status); | ||
|
|
||
| -- === workspace_twilio_config.sql === | ||
| CREATE TABLE IF NOT EXISTS workspace_twilio_config ( | ||
| workspace_id UUID PRIMARY KEY, | ||
| mode VARCHAR(10) NOT NULL DEFAULT 'hosted' CHECK (mode IN ('hosted', 'byok')), | ||
| sub_account_sid VARCHAR(255), | ||
| sub_account_token_encrypted TEXT, | ||
| byok_account_sid_encrypted TEXT, | ||
| byok_auth_token_encrypted TEXT, | ||
| byok_api_key_encrypted TEXT, | ||
| byok_api_secret_encrypted TEXT, | ||
| twiml_app_sid VARCHAR(255), | ||
| created_at TIMESTAMPTZ DEFAULT NOW(), | ||
| updated_at TIMESTAMPTZ DEFAULT NOW() | ||
| ); | ||
|
|
||
| -- === 002-knowledge-base (no pgvector) === | ||
| CREATE TABLE IF NOT EXISTS knowledge_collections ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id UUID NOT NULL, | ||
| name VARCHAR(255) NOT NULL, | ||
| description TEXT, | ||
| chunk_count INT DEFAULT 0, | ||
| created_at TIMESTAMPTZ DEFAULT NOW(), | ||
| updated_at TIMESTAMPTZ DEFAULT NOW(), | ||
| UNIQUE(workspace_id, name) | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_knowledge_collections_workspace ON knowledge_collections(workspace_id); | ||
|
|
||
| CREATE TABLE IF NOT EXISTS knowledge_chunks ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| collection_id UUID NOT NULL REFERENCES knowledge_collections(id) ON DELETE CASCADE, | ||
| file_id UUID NOT NULL REFERENCES files(id) ON DELETE CASCADE, | ||
| chunk_index INT NOT NULL, | ||
| content TEXT NOT NULL, | ||
| metadata JSONB DEFAULT '{}', | ||
| created_at TIMESTAMPTZ DEFAULT NOW() | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_chunks_collection ON knowledge_chunks(collection_id); | ||
| CREATE INDEX IF NOT EXISTS idx_chunks_file ON knowledge_chunks(file_id); | ||
| CREATE INDEX IF NOT EXISTS idx_chunks_metadata ON knowledge_chunks USING gin (metadata jsonb_path_ops); | ||
|
|
||
| ALTER TABLE files ADD COLUMN IF NOT EXISTS indexed_at TIMESTAMPTZ; | ||
| ALTER TABLE files ADD COLUMN IF NOT EXISTS collection_id UUID REFERENCES knowledge_collections(id); | ||
|
|
||
| COMMIT; |
There was a problem hiding this comment.
1. combined-all.sql missing down 📘 Rule violation ⛯ Reliability
This combined SQL migration script wraps multiple schema changes in a transaction but provides no rollback (down) script/section. This violates the requirement for migrations to include both up and down behavior.
Agent Prompt
## Issue description
`packages/api/src/migrations/combined-all.sql` applies a large set of schema changes but does not include any rollback (`down`) steps, violating migration rules requiring both `up` and `down`.
## Issue Context
The file begins with `BEGIN;` and ends with `COMMIT;`, but contains no inverse operations to revert the changes.
## Fix Focus Areas
- packages/api/src/migrations/combined-all.sql[1-483]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| plan_name VARCHAR(64), | ||
| interval VARCHAR(16), | ||
| current_period_end TIMESTAMPTZ, | ||
| add_ons JSONB DEFAULT '[]'::jsonb, |
There was a problem hiding this comment.
2. Jsonb add_ons parsed wrongly 🐞 Bug ✓ Correctness
workspace_subscriptions.add_ons is defined as JSONB, but subscription code JSON.parse()s it as if it were a string; with node-postgres JSONB is typically returned as a JS object/array, so this will throw and break subscription status endpoints.
Agent Prompt
### Issue description
`workspace_subscriptions.add_ons` is created as `JSONB`, but the subscription service reads it as a string and calls `JSON.parse(...)`, which will throw when the driver returns JSONB as a JS array/object.
### Issue Context
- The schema declares `add_ons JSONB`.
- The service both writes `JSON.stringify(addOns)` and reads with `JSON.parse(sub.add_ons as string)`.
### Fix Focus Areas
- packages/api/src/services/subscription.ts[123-134]
- packages/api/src/services/subscription.ts[250-270]
- packages/api/src/migrations/017-create-workspace-subscriptions.sql[1-13] (only if you choose to change column type instead)
### Suggested change
- Prefer keeping JSONB and updating read logic:
- If `sub.add_ons` is already an array/object, use it directly.
- Otherwise (for backward compatibility), parse `String(sub.add_ons)`.
- (Optional) Update writes to pass the JS array/object rather than `JSON.stringify`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
Summary by CodeRabbit
WalkthroughAdds new SQL migrations creating many workspace-scoped tables (calls, contacts, subscriptions/usage, files, GHL and assistant-related tables, knowledge base, etc.), swaps several frontend fetch calls to use Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (9)
packages/api/src/migrations/016-create-contacts.sql (1)
1-22: Good schema design; consider uniqueness constraints.The contacts table structure is solid with appropriate indexes for common lookups.
dnc_statusdefault of'allowed'is good for DNC compliance workflows.Consider whether unique constraints are needed to prevent duplicate contacts:
-- If contacts should be unique per workspace by phone or email: CREATE UNIQUE INDEX IF NOT EXISTS idx_contacts_workspace_phone_unique ON contacts(workspace_id, phone) WHERE phone IS NOT NULL; CREATE UNIQUE INDEX IF NOT EXISTS idx_contacts_workspace_email_unique ON contacts(workspace_id, email) WHERE email IS NOT NULL;This depends on business requirements—if duplicates are allowed (e.g., same phone across different contact records), skip this.
Would you like me to open an issue to track whether uniqueness constraints are needed for the contacts table?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/api/src/migrations/016-create-contacts.sql` around lines 1 - 22, Add optional partial unique indexes to prevent duplicate contacts per workspace by phone or email: create UNIQUE INDEXes named idx_contacts_workspace_phone_unique on (workspace_id, phone) WHERE phone IS NOT NULL and idx_contacts_workspace_email_unique on (workspace_id, email) WHERE email IS NOT NULL; place these after the existing CREATE INDEX statements for contacts and ensure they are conditional (IF NOT EXISTS) so they do not error on repeated runs—only add them if business logic requires a single contact per workspace per phone/email.packages/twenty-front/src/pages/settings/consuelo/GHLSettings.tsx (1)
65-79: Migration looks correct; consider defensive header merging.The switch to
authenticatedFetchworks well here. Minor note: if future callers passheadersinoptions, they would overrideContent-Type. Current usage is safe, but consider merging headers defensively.♻️ Defensive header merge (optional)
const fetchJson = async <TData,>( path: string, options?: RequestInit, ): Promise<TData> => { + const headers = { + 'Content-Type': 'application/json', + ...(options?.headers as Record<string, string> ?? {}), + }; const res = await authenticatedFetch(`${REACT_APP_SERVER_BASE_URL}${path}`, { - headers: { 'Content-Type': 'application/json' }, ...options, + headers, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/twenty-front/src/pages/settings/consuelo/GHLSettings.tsx` around lines 65 - 79, fetchJson currently spreads options after setting default headers so callers' headers can unintentionally override Content-Type; change fetchJson to build a merged headers object (e.g., default {'Content-Type':'application/json'} overridden by options.headers) and pass that merged headers into authenticatedFetch along with the rest of options (ensure you don't double-include headers). Update the implementation inside fetchJson (referencing fetchJson, authenticatedFetch and REACT_APP_SERVER_BASE_URL) to compute mergedHeaders from options?.headers and use it in the request.packages/twenty-server/src/main.ts (1)
145-151: Good CORS fix, but note inconsistency with UnhandledExceptionFilter.The origin-reflecting pattern correctly supports credentialed requests for Express-mounted routes. However,
UnhandledExceptionFilter(seepackages/twenty-server/src/filters/unhandled-exception.filter.tsLine 24) usesAccess-Control-Allow-Origin: *, which is incompatible withcredentials: 'include'. Consider unifying the approach.♻️ Suggested: Extract shared CORS helper
// could be added to a shared utility const setCorsHeaders = (req: Request, res: Response) => { const origin = req.headers.origin; if (origin) { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Access-Control-Allow-Credentials', 'true'); } };Then update both this error handler and
UnhandledExceptionFilterto use the same logic.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/twenty-server/src/main.ts` around lines 145 - 151, Create a shared helper (e.g., setCorsHeaders(req, res)) that reads req.headers.origin and, if present, sets Access-Control-Allow-Origin to that origin and Access-Control-Allow-Credentials to 'true'; replace the inline origin-reflecting code in main.ts and the wildcard header in UnhandledExceptionFilter (class UnhandledExceptionFilter, likely its catch/handle method) to call this helper so both error paths use the same origin-reflecting, credential-compatible CORS behavior.packages/api/src/migrations/017-create-workspace-subscriptions.sql (3)
15-23: Missing foreign key constraint onworkspace_id.The
workspace_usagetable referencesworkspace_idwithout a foreign key constraint. If there's a canonicalworkspacestable (orworkspace_subscriptionsas the parent), consider adding a FK to enforce referential integrity and enable cascading deletes when a workspace is removed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/api/src/migrations/017-create-workspace-subscriptions.sql` around lines 15 - 23, Add a foreign key constraint on workspace_usage.workspace_id referencing the canonical parent table (e.g. workspaces.id or workspace_subscriptions.workspace_id) to enforce referential integrity; update the CREATE TABLE (or run an ALTER TABLE) to add FOREIGN KEY (workspace_id) REFERENCES workspaces(id) (or workspace_subscriptions(workspace_id)) and include ON DELETE CASCADE if you want workspace deletions to remove related workspace_usage rows, ensuring the constraint name is unique (e.g. workspace_usage_workspace_id_fkey).
1-25: Migration lacks transaction wrapper.Unlike
combined-all.sqlwhich wraps everything inBEGIN/COMMIT, this standalone migration has no transaction boundary. If this migration runs independently, a partial failure could leave the schema in an inconsistent state.💡 Wrap migration in a transaction
+BEGIN; + -- workspace_subscriptions: stripe subscription state per workspace CREATE TABLE IF NOT EXISTS workspace_subscriptions ( ... ); ... CREATE INDEX IF NOT EXISTS idx_workspace_usage_lookup ON workspace_usage(workspace_id, metric, period_start); + +COMMIT;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/api/src/migrations/017-create-workspace-subscriptions.sql` around lines 1 - 25, The migration currently creates workspace_subscriptions, workspace_usage, and idx_workspace_usage_lookup without a transaction boundary; wrap the entire SQL in a transaction by adding a BEGIN at the top and a COMMIT at the end so that creation of workspace_subscriptions, workspace_usage, and the index idx_workspace_usage_lookup are atomic and will roll back on failure.
1-13: Consider adding a unique constraint onstripe_subscription_id.If each Stripe subscription should map to exactly one workspace, adding a unique constraint prevents accidental duplicates that could cause billing inconsistencies.
Also,
stripe_customer_idandstripe_subscription_idlackNOT NULLconstraints. If a subscription record exists, these fields are typically required—consider whetherNULLis a valid state here.💡 Suggested improvement
CREATE TABLE IF NOT EXISTS workspace_subscriptions ( workspace_id UUID PRIMARY KEY, - stripe_customer_id VARCHAR(255), - stripe_subscription_id VARCHAR(255), + stripe_customer_id VARCHAR(255) NOT NULL, + stripe_subscription_id VARCHAR(255) NOT NULL UNIQUE, status VARCHAR(32) NOT NULL DEFAULT 'inactive',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/api/src/migrations/017-create-workspace-subscriptions.sql` around lines 1 - 13, The workspace_subscriptions table should enforce uniqueness and requiredness for Stripe identifiers: add a UNIQUE constraint/index on stripe_subscription_id (so one Stripe subscription maps to at most one workspace) and make stripe_subscription_id and, if appropriate for your domain, stripe_customer_id NOT NULL in the CREATE TABLE statement (or add an ALTER TABLE to add NOT NULL and the UNIQUE index if you must preserve existing migration history); update the migration definition that defines workspace_subscriptions and the column definitions for stripe_subscription_id and stripe_customer_id accordingly.packages/api/src/migrations/combined-all.sql (3)
379-379: Redundant indexes oncallstable.
idx_calls_workspace_start_time(line 379) andidx_calls_workspace_date(line 385) are functionally similar—both index(workspace_id, start_time). The only difference is DESC ordering on line 379. PostgreSQL can use a B-tree index for both ASC and DESC scans, so one of these indexes is likely redundant.Also applies to: 385-385
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/api/src/migrations/combined-all.sql` at line 379, The migration creates two redundant B-tree indexes on calls: idx_calls_workspace_start_time and idx_calls_workspace_date both on (workspace_id, start_time) (one with DESC ordering); remove one of them (or consolidate into a single index) to avoid duplication—prefer a single index named (keep either idx_calls_workspace_start_time or idx_calls_workspace_date) on (workspace_id, start_time) without duplicate definitions (or with the ordering you actually need), and update the SQL migration to drop the redundant CREATE INDEX line and any references that expect both indexes.
164-164:intervalis a reserved keyword in PostgreSQL.Using
intervalas a column name (line 164) requires quoting in queries. Consider renaming toplan_intervalorbilling_intervalto avoid potential issues.💡 Suggested rename
- interval VARCHAR(16), + plan_interval VARCHAR(16),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/api/src/migrations/combined-all.sql` at line 164, The schema uses a column named interval (see the column declaration "interval VARCHAR(16)"), which conflicts with PostgreSQL reserved words; rename this column to a non-reserved identifier (e.g., plan_interval or billing_interval) in the combined-all.sql migration and update every reference to it (table DDL, INSERTs, SELECTs, ALTERs, constraints, indexes, and any ORM/entity mappings or query code that refers to "interval") so queries no longer require quoting; ensure the migration preserves data (use ALTER TABLE ... RENAME COLUMN if migrating an existing DB) and update all functions, views, and tests that reference the old name.
126-134: Missing foreign key oncontact_idincontact_notesandcontact_follow_ups.Both tables reference
contact_idbut lack FK constraints to thecontactstable. This could lead to orphaned records if contacts are deleted.💡 Add FK constraints
CREATE TABLE IF NOT EXISTS contact_notes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - contact_id UUID NOT NULL, + contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, ... ); CREATE TABLE IF NOT EXISTS contact_follow_ups ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - contact_id UUID NOT NULL, + contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, ... );Also applies to: 139-150
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/api/src/migrations/combined-all.sql` around lines 126 - 134, Add missing foreign key constraints for the contact_id columns: update the CREATE TABLE definitions for contact_notes and contact_follow_ups to include FOREIGN KEY (contact_id) REFERENCES contacts(id) (optionally with ON DELETE CASCADE or ON DELETE RESTRICT depending on desired behavior) so contact_id in contact_notes and contact_follow_ups cannot point to non-existent rows in contacts; ensure the constraint names are unique (e.g., contact_notes_contact_id_fkey, contact_follow_ups_contact_id_fkey) and apply the change either inline in the CREATE TABLE statements or via ALTER TABLE if preferred.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/api/src/migrations/combined-all.sql`:
- Line 94: The migration defines the same index name idx_calls_contact twice
(once as CREATE INDEX IF NOT EXISTS idx_calls_contact ON calls(contact_id) and
again as a partial index WHERE contact_id IS NOT NULL), which prevents the
partial index from being created; update the migration so the indexes have
distinct names (e.g., idx_calls_contact_full for the full index and
idx_calls_contact_not_null for the partial index) or remove the undesired
duplicate, and ensure the CREATE INDEX statements reference the correct names
for the full index and the partial index on calls(contact_id) and WHERE
contact_id IS NOT NULL respectively.
- Around line 7-19: The migration defines `workspace_id` with inconsistent
types; update every table in this migration (e.g., files, calls, contacts,
workspace_subscriptions, workspace_usage, ghl_connections,
assistant_conversations, ghl_pipeline_mappings, user_settings,
workspace_settings, workspace_members, call_queues, etc.) so `workspace_id` is
uniformly UUID (use "UUID" as the column type), set NOT NULL or NULL to match
intended semantics, and where appropriate add DEFAULT gen_random_uuid() or a
FOREIGN KEY reference to the workspaces table to match other tables; edit the
CREATE TABLE statements in combined-all.sql to change any VARCHAR(255) or TEXT
workspace_id definitions to UUID and ensure indexes/constraints referencing
workspace_id use UUID as well.
---
Nitpick comments:
In `@packages/api/src/migrations/016-create-contacts.sql`:
- Around line 1-22: Add optional partial unique indexes to prevent duplicate
contacts per workspace by phone or email: create UNIQUE INDEXes named
idx_contacts_workspace_phone_unique on (workspace_id, phone) WHERE phone IS NOT
NULL and idx_contacts_workspace_email_unique on (workspace_id, email) WHERE
email IS NOT NULL; place these after the existing CREATE INDEX statements for
contacts and ensure they are conditional (IF NOT EXISTS) so they do not error on
repeated runs—only add them if business logic requires a single contact per
workspace per phone/email.
In `@packages/api/src/migrations/017-create-workspace-subscriptions.sql`:
- Around line 15-23: Add a foreign key constraint on
workspace_usage.workspace_id referencing the canonical parent table (e.g.
workspaces.id or workspace_subscriptions.workspace_id) to enforce referential
integrity; update the CREATE TABLE (or run an ALTER TABLE) to add FOREIGN KEY
(workspace_id) REFERENCES workspaces(id) (or
workspace_subscriptions(workspace_id)) and include ON DELETE CASCADE if you want
workspace deletions to remove related workspace_usage rows, ensuring the
constraint name is unique (e.g. workspace_usage_workspace_id_fkey).
- Around line 1-25: The migration currently creates workspace_subscriptions,
workspace_usage, and idx_workspace_usage_lookup without a transaction boundary;
wrap the entire SQL in a transaction by adding a BEGIN at the top and a COMMIT
at the end so that creation of workspace_subscriptions, workspace_usage, and the
index idx_workspace_usage_lookup are atomic and will roll back on failure.
- Around line 1-13: The workspace_subscriptions table should enforce uniqueness
and requiredness for Stripe identifiers: add a UNIQUE constraint/index on
stripe_subscription_id (so one Stripe subscription maps to at most one
workspace) and make stripe_subscription_id and, if appropriate for your domain,
stripe_customer_id NOT NULL in the CREATE TABLE statement (or add an ALTER TABLE
to add NOT NULL and the UNIQUE index if you must preserve existing migration
history); update the migration definition that defines workspace_subscriptions
and the column definitions for stripe_subscription_id and stripe_customer_id
accordingly.
In `@packages/api/src/migrations/combined-all.sql`:
- Line 379: The migration creates two redundant B-tree indexes on calls:
idx_calls_workspace_start_time and idx_calls_workspace_date both on
(workspace_id, start_time) (one with DESC ordering); remove one of them (or
consolidate into a single index) to avoid duplication—prefer a single index
named (keep either idx_calls_workspace_start_time or idx_calls_workspace_date)
on (workspace_id, start_time) without duplicate definitions (or with the
ordering you actually need), and update the SQL migration to drop the redundant
CREATE INDEX line and any references that expect both indexes.
- Line 164: The schema uses a column named interval (see the column declaration
"interval VARCHAR(16)"), which conflicts with PostgreSQL reserved words; rename
this column to a non-reserved identifier (e.g., plan_interval or
billing_interval) in the combined-all.sql migration and update every reference
to it (table DDL, INSERTs, SELECTs, ALTERs, constraints, indexes, and any
ORM/entity mappings or query code that refers to "interval") so queries no
longer require quoting; ensure the migration preserves data (use ALTER TABLE ...
RENAME COLUMN if migrating an existing DB) and update all functions, views, and
tests that reference the old name.
- Around line 126-134: Add missing foreign key constraints for the contact_id
columns: update the CREATE TABLE definitions for contact_notes and
contact_follow_ups to include FOREIGN KEY (contact_id) REFERENCES contacts(id)
(optionally with ON DELETE CASCADE or ON DELETE RESTRICT depending on desired
behavior) so contact_id in contact_notes and contact_follow_ups cannot point to
non-existent rows in contacts; ensure the constraint names are unique (e.g.,
contact_notes_contact_id_fkey, contact_follow_ups_contact_id_fkey) and apply the
change either inline in the CREATE TABLE statements or via ALTER TABLE if
preferred.
In `@packages/twenty-front/src/pages/settings/consuelo/GHLSettings.tsx`:
- Around line 65-79: fetchJson currently spreads options after setting default
headers so callers' headers can unintentionally override Content-Type; change
fetchJson to build a merged headers object (e.g., default
{'Content-Type':'application/json'} overridden by options.headers) and pass that
merged headers into authenticatedFetch along with the rest of options (ensure
you don't double-include headers). Update the implementation inside fetchJson
(referencing fetchJson, authenticatedFetch and REACT_APP_SERVER_BASE_URL) to
compute mergedHeaders from options?.headers and use it in the request.
In `@packages/twenty-server/src/main.ts`:
- Around line 145-151: Create a shared helper (e.g., setCorsHeaders(req, res))
that reads req.headers.origin and, if present, sets Access-Control-Allow-Origin
to that origin and Access-Control-Allow-Credentials to 'true'; replace the
inline origin-reflecting code in main.ts and the wildcard header in
UnhandledExceptionFilter (class UnhandledExceptionFilter, likely its
catch/handle method) to call this helper so both error paths use the same
origin-reflecting, credential-compatible CORS behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 50fc7a49-575b-4f6e-a4ee-acc8c525efdb
📒 Files selected for processing (10)
packages/api/src/migrations/015-create-calls.sqlpackages/api/src/migrations/016-create-contacts.sqlpackages/api/src/migrations/017-create-workspace-subscriptions.sqlpackages/api/src/migrations/combined-all.sqlpackages/twenty-front/src/modules/assistant/commands/handlers.tspackages/twenty-front/src/modules/assistant/hooks/useAssistant.tspackages/twenty-front/src/modules/settings/hooks/useUserPreferences.tspackages/twenty-front/src/pages/settings/consuelo/GHLSettings.tsxpackages/twenty-front/src/pages/settings/consuelo/TwilioSettings.tsxpackages/twenty-server/src/main.ts
| CREATE TABLE IF NOT EXISTS files ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| workspace_id UUID NOT NULL, | ||
| name VARCHAR(255) NOT NULL, | ||
| mime_type VARCHAR(100) NOT NULL, | ||
| size BIGINT NOT NULL, | ||
| storage_key VARCHAR(500) NOT NULL, | ||
| folder VARCHAR(255), | ||
| tags TEXT[], | ||
| uploaded_by UUID, | ||
| created_at TIMESTAMPTZ DEFAULT NOW(), | ||
| updated_at TIMESTAMPTZ DEFAULT NOW() | ||
| ); |
There was a problem hiding this comment.
Inconsistent workspace_id column types across tables.
The schema uses three different types for workspace_id:
UUID: files, calls, contacts, workspace_subscriptions, workspace_usage, ghl_connections, assistant_conversations, ghl_pipeline_mappings, etc.VARCHAR(255): user_settings (line 189), workspace_settings (line 206), workspace_members (line 219)TEXT: call_queues (line 397)
This inconsistency will cause join failures and complicate queries across these tables. Standardize on UUID for consistency with the majority of tables.
🔧 Example fixes for VARCHAR/TEXT tables
CREATE TABLE IF NOT EXISTS user_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id VARCHAR(255) NOT NULL,
- workspace_id VARCHAR(255) NOT NULL,
+ workspace_id UUID NOT NULL,
...
);
CREATE TABLE IF NOT EXISTS workspace_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- workspace_id VARCHAR(255) NOT NULL UNIQUE,
+ workspace_id UUID NOT NULL UNIQUE,
...
);
CREATE TABLE IF NOT EXISTS workspace_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- workspace_id VARCHAR(255) NOT NULL,
+ workspace_id UUID NOT NULL,
...
);
CREATE TABLE IF NOT EXISTS call_queues (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- workspace_id TEXT NOT NULL,
+ workspace_id UUID NOT NULL,
...
);Also applies to: 63-89, 101-117, 158-169, 186-194, 204-213, 217-228, 395-416
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/api/src/migrations/combined-all.sql` around lines 7 - 19, The
migration defines `workspace_id` with inconsistent types; update every table in
this migration (e.g., files, calls, contacts, workspace_subscriptions,
workspace_usage, ghl_connections, assistant_conversations,
ghl_pipeline_mappings, user_settings, workspace_settings, workspace_members,
call_queues, etc.) so `workspace_id` is uniformly UUID (use "UUID" as the column
type), set NOT NULL or NULL to match intended semantics, and where appropriate
add DEFAULT gen_random_uuid() or a FOREIGN KEY reference to the workspaces table
to match other tables; edit the CREATE TABLE statements in combined-all.sql to
change any VARCHAR(255) or TEXT workspace_id definitions to UUID and ensure
indexes/constraints referencing workspace_id use UUID as well.
| CREATE INDEX IF NOT EXISTS idx_calls_workspace ON calls(workspace_id); | ||
| CREATE INDEX IF NOT EXISTS idx_calls_call_sid ON calls(call_sid); | ||
| CREATE INDEX IF NOT EXISTS idx_calls_start_time ON calls(workspace_id, start_time DESC); | ||
| CREATE INDEX IF NOT EXISTS idx_calls_contact ON calls(contact_id); |
There was a problem hiding this comment.
Duplicate index definition for idx_calls_contact.
Line 94 creates idx_calls_contact ON calls(contact_id), and line 388 creates the same index name with a WHERE contact_id IS NOT NULL filter. While IF NOT EXISTS prevents an error, the second (partial) index won't be created, which may not be the intended behavior.
If you want both a full index and a partial index, use distinct names.
🔧 Suggested fix
-CREATE INDEX IF NOT EXISTS idx_calls_contact ON calls(contact_id) WHERE contact_id IS NOT NULL;
+CREATE INDEX IF NOT EXISTS idx_calls_contact_partial ON calls(contact_id) WHERE contact_id IS NOT NULL;Also applies to: 388-388
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/api/src/migrations/combined-all.sql` at line 94, The migration
defines the same index name idx_calls_contact twice (once as CREATE INDEX IF NOT
EXISTS idx_calls_contact ON calls(contact_id) and again as a partial index WHERE
contact_id IS NOT NULL), which prevents the partial index from being created;
update the migration so the indexes have distinct names (e.g.,
idx_calls_contact_full for the full index and idx_calls_contact_not_null for the
partial index) or remove the undesired duplicate, and ensure the CREATE INDEX
statements reference the correct names for the full index and the partial index
on calls(contact_id) and WHERE contact_id IS NOT NULL respectively.
twilio was only a peerDep of @consuelo/dialer and devDep of dialer. yarn workspaces focus --production strips devDeps, leaving twilio missing in the Docker image. This caused GET /v1/phone-numbers to 500 with a module-not-found error. Fixes phone-numbers 500 in production.
|
🚀 Preview Environment Ready! Your preview environment is available at: http://bore.pub:32000 This environment will automatically shut down when the PR is closed or after 5 hours. |
No description provided.