feat: integrate Supabase for database operations and migrations
Add support for Supabase database operations, including migrations and queries. Implement new Supabase-related types, actions, and components to handle database interactions. Enhance the prompt system to include Supabase-specific instructions and constraints. Ensure data integrity and security by enforcing row-level security and proper migration practices.
This commit is contained in:
@@ -12,7 +12,12 @@ import { getFilePaths } from './select-context';
|
||||
|
||||
export type Messages = Message[];
|
||||
|
||||
export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
|
||||
export interface StreamingOptions extends Omit<Parameters<typeof _streamText>[0], 'model'> {
|
||||
supabaseConnection?: {
|
||||
isConnected: boolean;
|
||||
hasSelectedProject: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const logger = createScopedLogger('stream-text');
|
||||
|
||||
@@ -97,6 +102,10 @@ export async function streamText(props: {
|
||||
cwd: WORK_DIR,
|
||||
allowedHtmlElements: allowedHTMLElements,
|
||||
modificationTagName: MODIFICATIONS_TAG_NAME,
|
||||
supabase: {
|
||||
isConnected: options?.supabaseConnection?.isConnected || false,
|
||||
hasSelectedProject: options?.supabaseConnection?.hasSelectedProject || false,
|
||||
},
|
||||
}) ?? getSystemPrompt();
|
||||
|
||||
if (files && contextFiles && contextOptimization) {
|
||||
|
||||
@@ -5,6 +5,10 @@ export interface PromptOptions {
|
||||
cwd: string;
|
||||
allowedHtmlElements: string[];
|
||||
modificationTagName: string;
|
||||
supabase?: {
|
||||
isConnected: boolean;
|
||||
hasSelectedProject: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export class PromptLibrary {
|
||||
@@ -19,7 +23,7 @@ export class PromptLibrary {
|
||||
default: {
|
||||
label: 'Default Prompt',
|
||||
description: 'This is the battle tested default system Prompt',
|
||||
get: (options) => getSystemPrompt(options.cwd),
|
||||
get: (options) => getSystemPrompt(options.cwd, options.supabase),
|
||||
},
|
||||
optimized: {
|
||||
label: 'Optimized Prompt (experimental)',
|
||||
|
||||
@@ -2,7 +2,10 @@ import { WORK_DIR } from '~/utils/constants';
|
||||
import { allowedHTMLElements } from '~/utils/markdown';
|
||||
import { stripIndents } from '~/utils/stripIndent';
|
||||
|
||||
export const getSystemPrompt = (cwd: string = WORK_DIR) => `
|
||||
export const getSystemPrompt = (
|
||||
cwd: string = WORK_DIR,
|
||||
supabase?: { isConnected: boolean; hasSelectedProject: boolean },
|
||||
) => `
|
||||
You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices.
|
||||
|
||||
<system_constraints>
|
||||
@@ -59,6 +62,195 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
|
||||
- curl, head, sort, tail, clear, which, export, chmod, scho, hostname, kill, ln, xxd, alias, false, getconf, true, loadenv, wasm, xdg-open, command, exit, source
|
||||
</system_constraints>
|
||||
|
||||
<database_instructions>
|
||||
The following instructions guide how you should handle database operations in projects.
|
||||
|
||||
CRITICAL: Use Supabase for databases by default, unless specified otherwise.
|
||||
|
||||
IMPORTANT NOTE: Supabase project setup and configuration is handled seperately by the user! ${
|
||||
supabase
|
||||
? !supabase.isConnected
|
||||
? 'You are not connected to Supabase. Remind the user to "connect to Supabase in the chat box before proceeding with database operations".'
|
||||
: !supabase.hasSelectedProject
|
||||
? 'Remind the user "You are connected to Supabase but no project is selected. Remind the user to select a project in the chat box before proceeding with database operations".'
|
||||
: ''
|
||||
: ''
|
||||
}
|
||||
The environment variables for Supabase connection will be available in the project's \`.env\` file.
|
||||
IMPORTANT: Create a .env file if it doesnt exist.
|
||||
NEVER modify any Supabase configuration or \`.env\` files.
|
||||
|
||||
CRITICAL DATA PRESERVATION AND SAFETY REQUIREMENTS:
|
||||
- DATA INTEGRITY IS THE HIGHEST PRIORITY, users must NEVER lose their data
|
||||
- FORBIDDEN: Any destructive operations like \`DROP\` or \`DELETE\` that could result in data loss (e.g., when dropping columns, changing column types, renaming tables, etc.)
|
||||
- FORBIDDEN: Any transaction control statements (e.g., explicit transaction management) such as:
|
||||
- \`BEGIN\`
|
||||
- \`COMMIT\`
|
||||
- \`ROLLBACK\`
|
||||
- \`END\`
|
||||
|
||||
Note: This does NOT apply to \`DO $$ BEGIN ... END $$\` blocks, which are PL/pgSQL anonymous blocks!
|
||||
|
||||
Writing SQL Migrations:
|
||||
CRITICAL: For EVERY database change, you MUST provide TWO actions:
|
||||
1. Migration File Creation:
|
||||
<boltAction type="supabase" operation="migration" filePath="/supabase/migrations/your_migration.sql">
|
||||
/* SQL migration content */
|
||||
</boltAction>
|
||||
|
||||
2. Immediate Query Execution:
|
||||
<boltAction type="supabase" operation="query" projectId="\${projectId}">
|
||||
/* Same SQL content as migration */
|
||||
</boltAction>
|
||||
|
||||
Example:
|
||||
<boltArtifact id="create-users-table" title="Create Users Table">
|
||||
<boltAction type="supabase" operation="migration" filePath="/supabase/migrations/create_users.sql">
|
||||
CREATE TABLE users (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email text UNIQUE NOT NULL
|
||||
);
|
||||
</boltAction>
|
||||
|
||||
<boltAction type="supabase" operation="query" projectId="\${projectId}">
|
||||
CREATE TABLE users (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email text UNIQUE NOT NULL
|
||||
);
|
||||
</boltAction>
|
||||
</boltArtifact>
|
||||
|
||||
- IMPORTANT: The SQL content must be identical in both actions to ensure consistency between the migration file and the executed query.
|
||||
- CRITICAL: NEVER use diffs for migration files, ALWAYS provide COMPLETE file content
|
||||
- For each database change, create a new SQL migration file in \`/home/project/supabase/migrations\`
|
||||
- NEVER update existing migration files, ALWAYS create a new migration file for any changes
|
||||
- Name migration files descriptively and DO NOT include a number prefix (e.g., \`create_users.sql\`, \`add_posts_table.sql\`).
|
||||
|
||||
- DO NOT worry about ordering as the files will be renamed correctly!
|
||||
|
||||
- ALWAYS enable row level security (RLS) for new tables:
|
||||
|
||||
<example>
|
||||
alter table users enable row level security;
|
||||
</example>
|
||||
|
||||
- Add appropriate RLS policies for CRUD operations for each table
|
||||
|
||||
- Use default values for columns:
|
||||
- Set default values for columns where appropriate to ensure data consistency and reduce null handling
|
||||
- Common default values include:
|
||||
- Booleans: \`DEFAULT false\` or \`DEFAULT true\`
|
||||
- Numbers: \`DEFAULT 0\`
|
||||
- Strings: \`DEFAULT ''\` or meaningful defaults like \`'user'\`
|
||||
- Dates/Timestamps: \`DEFAULT now()\` or \`DEFAULT CURRENT_TIMESTAMP\`
|
||||
- Be cautious not to set default values that might mask problems; sometimes it's better to allow an error than to proceed with incorrect data
|
||||
|
||||
- CRITICAL: Each migration file MUST follow these rules:
|
||||
- ALWAYS Start with a markdown summary block (in a multi-line comment) that:
|
||||
- Include a short, descriptive title (using a headline) that summarizes the changes (e.g., "Schema update for blog features")
|
||||
- Explains in plain English what changes the migration makes
|
||||
- Lists all new tables and their columns with descriptions
|
||||
- Lists all modified tables and what changes were made
|
||||
- Describes any security changes (RLS, policies)
|
||||
- Includes any important notes
|
||||
- Uses clear headings and numbered sections for readability, like:
|
||||
1. New Tables
|
||||
2. Security
|
||||
3. Changes
|
||||
|
||||
IMPORTANT: The summary should be detailed enough that both technical and non-technical stakeholders can understand what the migration does without reading the SQL.
|
||||
|
||||
- Include all necessary operations (e.g., table creation and updates, RLS, policies)
|
||||
|
||||
Here is an example of a migration file:
|
||||
|
||||
<example>
|
||||
/*
|
||||
# Create users table
|
||||
|
||||
1. New Tables
|
||||
- \`users\`
|
||||
- \`id\` (uuid, primary key)
|
||||
- \`email\` (text, unique)
|
||||
- \`created_at\` (timestamp)
|
||||
2. Security
|
||||
- Enable RLS on \`users\` table
|
||||
- Add policy for authenticated users to read their own data
|
||||
*/
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email text UNIQUE NOT NULL,
|
||||
created_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "Users can read own data"
|
||||
ON users
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (auth.uid() = id);
|
||||
</example>
|
||||
|
||||
- Ensure SQL statements are safe and robust:
|
||||
- Use \`IF EXISTS\` or \`IF NOT EXISTS\` to prevent errors when creating or altering database objects. Here are examples:
|
||||
|
||||
<example>
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email text UNIQUE NOT NULL,
|
||||
created_at timestamptz DEFAULT now()
|
||||
);
|
||||
</example>
|
||||
|
||||
<example>
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND column_name = 'last_login'
|
||||
) THEN
|
||||
ALTER TABLE users ADD COLUMN last_login timestamptz;
|
||||
END IF;
|
||||
END $$;
|
||||
</example>
|
||||
|
||||
Client Setup:
|
||||
- Use \`@supabase/supabase-js\`
|
||||
- Create a singleton client instance
|
||||
- Use the environment variables from the project's \`.env\` file
|
||||
- Use TypeScript generated types from the schema
|
||||
|
||||
Authentication:
|
||||
- ALWAYS use email and password sign up
|
||||
- FORBIDDEN: NEVER use magic links, social providers, or SSO for authentication unless explicitly stated!
|
||||
- FORBIDDEN: NEVER create your own authentication system or authentication table, ALWAYS use Supabase's built-in authentication!
|
||||
- Email confirmation is ALWAYS disabled unless explicitly stated!
|
||||
|
||||
Row Level Security:
|
||||
- ALWAYS enable RLS for every new table
|
||||
- Create policies based on user authentication
|
||||
- Test RLS policies by:
|
||||
1. Verifying authenticated users can only access their allowed data
|
||||
2. Confirming unauthenticated users cannot access protected data
|
||||
3. Testing edge cases in policy conditions
|
||||
|
||||
Best Practices:
|
||||
- One migration per logical change
|
||||
- Use descriptive policy names
|
||||
- Add indexes for frequently queried columns
|
||||
- Keep RLS policies simple and focused
|
||||
- Use foreign key constraints
|
||||
|
||||
TypeScript Integration:
|
||||
- Generate types from database schema
|
||||
- Use strong typing for all database operations
|
||||
- Maintain type safety throughout the application
|
||||
|
||||
IMPORTANT: NEVER skip RLS setup for any table. Security is non-negotiable!
|
||||
</database_instructions>
|
||||
|
||||
<code_formatting_info>
|
||||
Use 2 spaces for code indentation
|
||||
</code_formatting_info>
|
||||
|
||||
111
app/lib/hooks/useSupabaseConnection.ts
Normal file
111
app/lib/hooks/useSupabaseConnection.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { logStore } from '~/lib/stores/logs';
|
||||
import { supabaseConnection, isConnecting, isFetchingStats, updateSupabaseConnection } from '~/lib/stores/supabase';
|
||||
|
||||
export function useSupabaseConnection() {
|
||||
const connection = useStore(supabaseConnection);
|
||||
const connecting = useStore(isConnecting);
|
||||
const fetchingStats = useStore(isFetchingStats);
|
||||
const [isProjectsExpanded, setIsProjectsExpanded] = useState(false);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const savedConnection = localStorage.getItem('supabase_connection');
|
||||
|
||||
if (savedConnection) {
|
||||
const parsed = JSON.parse(savedConnection);
|
||||
updateSupabaseConnection(parsed);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleConnect = async () => {
|
||||
isConnecting.set(true);
|
||||
|
||||
try {
|
||||
const cleanToken = connection.token.trim();
|
||||
|
||||
const response = await fetch('/api/supabase', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: cleanToken,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = (await response.json()) as any;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to connect');
|
||||
}
|
||||
|
||||
updateSupabaseConnection({
|
||||
user: data.user,
|
||||
token: connection.token,
|
||||
stats: data.stats,
|
||||
});
|
||||
|
||||
toast.success('Successfully connected to Supabase');
|
||||
|
||||
// Keep the dialog open and expand the projects section
|
||||
setIsProjectsExpanded(true);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Connection error:', error);
|
||||
logStore.logError('Failed to authenticate with Supabase', { error });
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to connect to Supabase');
|
||||
updateSupabaseConnection({ user: null, token: '' });
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
isConnecting.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
updateSupabaseConnection({ user: null, token: '' });
|
||||
toast.success('Disconnected from Supabase');
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
const selectProject = (projectId: string) => {
|
||||
const currentState = supabaseConnection.get();
|
||||
let projectData = undefined;
|
||||
|
||||
if (projectId && currentState.stats?.projects) {
|
||||
projectData = currentState.stats.projects.find((project) => project.id === projectId);
|
||||
}
|
||||
|
||||
updateSupabaseConnection({
|
||||
selectedProjectId: projectId,
|
||||
project: projectData,
|
||||
});
|
||||
|
||||
toast.success('Project selected successfully');
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
const handleCreateProject = async () => {
|
||||
window.open('https://app.supabase.com/new/new-project', '_blank');
|
||||
};
|
||||
|
||||
return {
|
||||
connection,
|
||||
connecting,
|
||||
fetchingStats,
|
||||
isProjectsExpanded,
|
||||
setIsProjectsExpanded,
|
||||
isDropdownOpen,
|
||||
setIsDropdownOpen,
|
||||
handleConnect,
|
||||
handleDisconnect,
|
||||
selectProject,
|
||||
handleCreateProject,
|
||||
updateToken: (token: string) => updateSupabaseConnection({ ...connection, token }),
|
||||
isConnected: !!(connection.user && connection.token),
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { WebContainer } from '@webcontainer/api';
|
||||
import { path as nodePath } from '~/utils/path';
|
||||
import { atom, map, type MapStore } from 'nanostores';
|
||||
import type { ActionAlert, BoltAction, FileHistory } from '~/types/actions';
|
||||
import type { ActionAlert, BoltAction, FileHistory, SupabaseAction, SupabaseAlert } from '~/types/actions';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { unreachable } from '~/utils/unreachable';
|
||||
import type { ActionCallbackData } from './message-parser';
|
||||
@@ -70,16 +70,19 @@ export class ActionRunner {
|
||||
runnerId = atom<string>(`${Date.now()}`);
|
||||
actions: ActionsMap = map({});
|
||||
onAlert?: (alert: ActionAlert) => void;
|
||||
onSupabaseAlert?: (alert: SupabaseAlert) => void;
|
||||
buildOutput?: { path: string; exitCode: number; output: string };
|
||||
|
||||
constructor(
|
||||
webcontainerPromise: Promise<WebContainer>,
|
||||
getShellTerminal: () => BoltShell,
|
||||
onAlert?: (alert: ActionAlert) => void,
|
||||
onSupabaseAlert?: (alert: SupabaseAlert) => void,
|
||||
) {
|
||||
this.#webcontainer = webcontainerPromise;
|
||||
this.#shellTerminal = getShellTerminal;
|
||||
this.onAlert = onAlert;
|
||||
this.onSupabaseAlert = onSupabaseAlert;
|
||||
}
|
||||
|
||||
addAction(data: ActionCallbackData) {
|
||||
@@ -157,6 +160,21 @@ export class ActionRunner {
|
||||
await this.#runFileAction(action);
|
||||
break;
|
||||
}
|
||||
case 'supabase': {
|
||||
try {
|
||||
await this.handleSupabaseAction(action as SupabaseAction);
|
||||
} catch (error: any) {
|
||||
// Update action status
|
||||
this.#updateAction(actionId, {
|
||||
status: 'failed',
|
||||
error: error instanceof Error ? error.message : 'Supabase action failed',
|
||||
});
|
||||
|
||||
// Return early without re-throwing
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'build': {
|
||||
const buildOutput = await this.#runBuildAction(action);
|
||||
|
||||
@@ -377,4 +395,50 @@ export class ActionRunner {
|
||||
output,
|
||||
};
|
||||
}
|
||||
async handleSupabaseAction(action: SupabaseAction) {
|
||||
const { operation, content, filePath } = action;
|
||||
logger.debug('[Supabase Action]:', { operation, filePath, content });
|
||||
|
||||
switch (operation) {
|
||||
case 'migration':
|
||||
if (!filePath) {
|
||||
throw new Error('Migration requires a filePath');
|
||||
}
|
||||
|
||||
// Show alert for migration action
|
||||
this.onSupabaseAlert?.({
|
||||
type: 'info',
|
||||
title: 'Supabase Migration',
|
||||
description: `Create migration file: ${filePath}`,
|
||||
content,
|
||||
source: 'supabase',
|
||||
});
|
||||
|
||||
// Only create the migration file
|
||||
await this.#runFileAction({
|
||||
type: 'file',
|
||||
filePath,
|
||||
content,
|
||||
changeSource: 'supabase',
|
||||
} as any);
|
||||
return { success: true };
|
||||
|
||||
case 'query': {
|
||||
// Always show the alert and let the SupabaseAlert component handle connection state
|
||||
this.onSupabaseAlert?.({
|
||||
type: 'info',
|
||||
title: 'Supabase Query',
|
||||
description: 'Execute database query',
|
||||
content,
|
||||
source: 'supabase',
|
||||
});
|
||||
|
||||
// The actual execution will be triggered from SupabaseChatAlert
|
||||
return { pending: true };
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown operation: ${operation}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ActionType, BoltAction, BoltActionData, FileAction, ShellAction } from '~/types/actions';
|
||||
import type { ActionType, BoltAction, BoltActionData, FileAction, ShellAction, SupabaseAction } from '~/types/actions';
|
||||
import type { BoltArtifactData } from '~/types/artifact';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { unreachable } from '~/utils/unreachable';
|
||||
@@ -293,7 +293,27 @@ export class StreamingMessageParser {
|
||||
content: '',
|
||||
};
|
||||
|
||||
if (actionType === 'file') {
|
||||
if (actionType === 'supabase') {
|
||||
const operation = this.#extractAttribute(actionTag, 'operation');
|
||||
|
||||
if (!operation || !['migration', 'query'].includes(operation)) {
|
||||
logger.warn(`Invalid or missing operation for Supabase action: ${operation}`);
|
||||
throw new Error(`Invalid Supabase operation: ${operation}`);
|
||||
}
|
||||
|
||||
(actionAttributes as SupabaseAction).operation = operation as 'migration' | 'query';
|
||||
|
||||
if (operation === 'migration') {
|
||||
const filePath = this.#extractAttribute(actionTag, 'filePath');
|
||||
|
||||
if (!filePath) {
|
||||
logger.warn('Migration requires a filePath');
|
||||
throw new Error('Migration requires a filePath');
|
||||
}
|
||||
|
||||
(actionAttributes as SupabaseAction).filePath = filePath;
|
||||
}
|
||||
} else if (actionType === 'file') {
|
||||
const filePath = this.#extractAttribute(actionTag, 'filePath') as string;
|
||||
|
||||
if (!filePath) {
|
||||
|
||||
130
app/lib/stores/supabase.ts
Normal file
130
app/lib/stores/supabase.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { atom } from 'nanostores';
|
||||
import type { SupabaseUser, SupabaseStats } from '~/types/supabase';
|
||||
|
||||
export interface SupabaseProject {
|
||||
id: string;
|
||||
name: string;
|
||||
region: string;
|
||||
organization_id: string;
|
||||
status: string;
|
||||
database?: {
|
||||
host: string;
|
||||
version: string;
|
||||
postgres_engine: string;
|
||||
release_channel: string;
|
||||
};
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface SupabaseConnectionState {
|
||||
user: SupabaseUser | null;
|
||||
token: string;
|
||||
stats?: SupabaseStats;
|
||||
selectedProjectId?: string;
|
||||
isConnected?: boolean;
|
||||
project?: SupabaseProject; // Add the selected project data
|
||||
}
|
||||
|
||||
// Init from localStorage if available
|
||||
const savedConnection = typeof localStorage !== 'undefined' ? localStorage.getItem('supabase_connection') : null;
|
||||
|
||||
const initialState: SupabaseConnectionState = savedConnection
|
||||
? JSON.parse(savedConnection)
|
||||
: {
|
||||
user: null,
|
||||
token: '',
|
||||
stats: undefined,
|
||||
selectedProjectId: undefined,
|
||||
isConnected: false,
|
||||
project: undefined, // Initialize as undefined
|
||||
};
|
||||
|
||||
export const supabaseConnection = atom<SupabaseConnectionState>(initialState);
|
||||
|
||||
// After init, fetch stats if we have a token
|
||||
if (initialState.token && !initialState.stats) {
|
||||
fetchSupabaseStats(initialState.token).catch(console.error);
|
||||
}
|
||||
|
||||
export const isConnecting = atom(false);
|
||||
export const isFetchingStats = atom(false);
|
||||
|
||||
export function updateSupabaseConnection(connection: Partial<SupabaseConnectionState>) {
|
||||
const currentState = supabaseConnection.get();
|
||||
|
||||
// Set isConnected based on user presence AND token
|
||||
if (connection.user !== undefined || connection.token !== undefined) {
|
||||
const newUser = connection.user !== undefined ? connection.user : currentState.user;
|
||||
const newToken = connection.token !== undefined ? connection.token : currentState.token;
|
||||
connection.isConnected = !!(newUser && newToken);
|
||||
}
|
||||
|
||||
// Update the project data when selectedProjectId changes
|
||||
if (connection.selectedProjectId !== undefined) {
|
||||
if (connection.selectedProjectId && currentState.stats?.projects) {
|
||||
const selectedProject = currentState.stats.projects.find(
|
||||
(project) => project.id === connection.selectedProjectId,
|
||||
);
|
||||
|
||||
if (selectedProject) {
|
||||
connection.project = selectedProject;
|
||||
} else {
|
||||
// If project not found in stats but ID is provided, set a minimal project object
|
||||
connection.project = {
|
||||
id: connection.selectedProjectId,
|
||||
name: `Project ${connection.selectedProjectId.substring(0, 8)}...`,
|
||||
region: 'unknown',
|
||||
organization_id: '',
|
||||
status: 'active',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
} else if (connection.selectedProjectId === '') {
|
||||
// Clear the project when selectedProjectId is empty
|
||||
connection.project = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const newState = { ...currentState, ...connection };
|
||||
supabaseConnection.set(newState);
|
||||
|
||||
/*
|
||||
* Always save the connection state to localStorage to persist across chats
|
||||
* Always save the connection state to localStorage to persist across chats
|
||||
*/
|
||||
if (connection.user || connection.token || connection.selectedProjectId !== undefined) {
|
||||
localStorage.setItem('supabase_connection', JSON.stringify(newState));
|
||||
} else {
|
||||
localStorage.removeItem('supabase_connection');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSupabaseStats(token: string) {
|
||||
isFetchingStats.set(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.supabase.com/v1/projects', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch projects');
|
||||
}
|
||||
|
||||
const projects = (await response.json()) as any;
|
||||
|
||||
updateSupabaseConnection({
|
||||
stats: {
|
||||
projects,
|
||||
totalProjects: projects.length,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch Supabase stats:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
isFetchingStats.set(false);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import { extractRelativePath } from '~/utils/diff';
|
||||
import { description } from '~/lib/persistence';
|
||||
import Cookies from 'js-cookie';
|
||||
import { createSampler } from '~/utils/sampler';
|
||||
import type { ActionAlert } from '~/types/actions';
|
||||
import type { ActionAlert, SupabaseAlert } from '~/types/actions';
|
||||
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
@@ -50,6 +50,8 @@ export class WorkbenchStore {
|
||||
unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
|
||||
actionAlert: WritableAtom<ActionAlert | undefined> =
|
||||
import.meta.hot?.data.unsavedFiles ?? atom<ActionAlert | undefined>(undefined);
|
||||
supabaseAlert: WritableAtom<SupabaseAlert | undefined> =
|
||||
import.meta.hot?.data.unsavedFiles ?? atom<ActionAlert | undefined>(undefined);
|
||||
modifiedFiles = new Set<string>();
|
||||
artifactIdList: string[] = [];
|
||||
#globalExecutionQueue = Promise.resolve();
|
||||
@@ -60,6 +62,7 @@ export class WorkbenchStore {
|
||||
import.meta.hot.data.showWorkbench = this.showWorkbench;
|
||||
import.meta.hot.data.currentView = this.currentView;
|
||||
import.meta.hot.data.actionAlert = this.actionAlert;
|
||||
import.meta.hot.data.supabaseAlert = this.supabaseAlert;
|
||||
|
||||
// Ensure binary files are properly preserved across hot reloads
|
||||
const filesMap = this.files.get();
|
||||
@@ -114,6 +117,14 @@ export class WorkbenchStore {
|
||||
this.actionAlert.set(undefined);
|
||||
}
|
||||
|
||||
get SupabaseAlert() {
|
||||
return this.supabaseAlert;
|
||||
}
|
||||
|
||||
clearSupabaseAlert() {
|
||||
this.supabaseAlert.set(undefined);
|
||||
}
|
||||
|
||||
toggleTerminal(value?: boolean) {
|
||||
this.#terminalStore.toggleTerminal(value);
|
||||
}
|
||||
@@ -405,6 +416,13 @@ export class WorkbenchStore {
|
||||
|
||||
this.actionAlert.set(alert);
|
||||
},
|
||||
(alert) => {
|
||||
if (this.#reloadedMessages.has(messageId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.supabaseAlert.set(alert);
|
||||
},
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user