UNPKG

@divetocode/supa-query-builder

Version:
2,128 lines (1,888 loc) 69 kB
# Unofficial Enhanced Supabase Client A comprehensive TypeScript client library that directly utilizes Supabase's REST API for advanced table schema management and CRUD operations. Provides an API similar to the official Supabase client while offering additional schema management capabilities. ## Key Features - 🔐 **Dual Client Architecture**: Browser client (ANON key) / Server client (SERVICE_ROLE key) separation - 📊 **Complete Schema Management**: Table creation, modification, deletion, and structure queries - 🔍 **Full CRUD Operations**: Create, Read, Update, Delete with advanced querying - 🛡️ **RLS Policy Management**: Comprehensive Row Level Security policy control - 📈 **Index Management**: Create, modify, and optimize database indexes - 🔗 **Foreign Key Support**: Full relational database constraint management - 🎯 **Type Safety**: Complete TypeScript support with robust type definitions - ⚡ **Promise-based API**: Modern async/await with consistent error handling ## Installation & Setup ```bash npm install @divetocode/supa-query-builder # or yarn add @divetocode/supa-query-builder ``` ```typescript import { SupabaseClient, SupabaseServer } from '@divetocode/supa-query-builder'; // Browser client (read operations focused) const supabase = new SupabaseClient( 'https://your-project.supabase.co', 'your-anon-key' ); // Server client (includes schema management) const supabaseServer = new SupabaseServer( 'https://your-project.supabase.co', 'your-anon-key', 'your-service-role-key' ); ``` ## 1. Table Schema Management ### 1.1 Querying Table Information ```typescript // Get all tables list const { data: tables, error } = await supabaseServer.schema().getTables(); // Get tables from specific schema const { data: publicTables } = await supabaseServer.schema().getTables('public'); console.log(tables); // [{ name: 'users', schema: 'public' }, { name: 'posts', schema: 'public' }] ``` ```typescript // Get detailed table information including columns const { data: tableInfo, error } = await supabaseServer.schema().getTableInfo('users'); console.log(tableInfo); /* Output example: { name: 'users', columns: [ { name: 'id', type: 'uuid', nullable: false, primaryKey: true }, { name: 'email', type: 'text', nullable: false, unique: true }, { name: 'name', type: 'text', nullable: true } ] } */ ``` ### 1.2 Table Creation ```typescript // Basic table creation const { data, error } = await supabaseServer.schema().createTable('posts', [ { name: 'id', type: 'uuid', primaryKey: true, defaultValue: 'gen_random_uuid()' }, { name: 'title', type: 'text', nullable: false }, { name: 'content', type: 'text', nullable: true }, { name: 'author_id', type: 'uuid', nullable: false, references: { table: 'users', column: 'id', onDelete: 'CASCADE' } }, { name: 'is_published', type: 'boolean', defaultValue: false }, { name: 'view_count', type: 'integer', defaultValue: 0 } ]); // Advanced table creation with options const { data: advancedTable } = await supabaseServer.schema().createTable('comments', [ { name: 'id', type: 'uuid', primaryKey: true, defaultValue: 'gen_random_uuid()' }, { name: 'post_id', type: 'uuid', references: { table: 'posts', column: 'id', onDelete: 'CASCADE' } }, { name: 'content', type: 'text', nullable: false } ], { schema: 'public', enableRLS: true, // Auto-enable RLS addCreatedAt: true, // Auto-add created_at column addUpdatedAt: true // Auto-add updated_at column }); ``` ### 1.3 Table Modifications #### Adding Columns ```typescript // Add new column await supabaseServer.schema().addColumn('posts', { name: 'tags', type: 'text[]', // Array type nullable: true }); // Add foreign key column await supabaseServer.schema().addColumn('posts', { name: 'category_id', type: 'uuid', references: { table: 'categories', column: 'id', onDelete: 'SET NULL' } }); // Add column with constraints await supabaseServer.schema().addColumn('posts', { name: 'slug', type: 'text', unique: true, nullable: false }); ``` #### Modifying Columns ```typescript // Change column type await supabaseServer.schema().alterColumn('posts', 'view_count', { type: 'bigint' }); // Change nullable constraint await supabaseServer.schema().alterColumn('posts', 'content', { nullable: false }); // Set default value await supabaseServer.schema().alterColumn('posts', 'is_featured', { defaultValue: false }); // Remove default value await supabaseServer.schema().alterColumn('posts', 'some_column', { dropDefault: true }); // Multiple changes at once await supabaseServer.schema().alterColumn('posts', 'status', { type: 'varchar(50)', nullable: false, defaultValue: 'draft' }); ``` #### Removing Columns ```typescript // Drop column const { data, error } = await supabaseServer.schema().dropColumn('posts', 'old_column'); if (error) { console.error('Failed to drop column:', error.message); } ``` #### Renaming ```typescript // Rename column await supabaseServer.schema().renameColumn('posts', 'content', 'body'); // Rename table await supabaseServer.schema().renameTable('posts', 'articles'); ``` ### 1.4 Table Operations ```typescript // Drop table (with cascade to remove dependent objects) const { data, error } = await supabaseServer.schema().dropTable('posts', 'public', true); // Simple table drop await supabaseServer.schema().dropTable('temp_table'); // Copy table structure only await supabaseServer.schema().copyTable('posts', 'posts_backup', false); // Copy table with data await supabaseServer.schema().copyTable('posts', 'posts_archive', true); // Check if table exists const { data: exists } = await supabaseServer.schema().tableExists('posts'); console.log(exists); // true or false ``` ## 2. Index Management ```typescript // Create single column index await supabaseServer.schema().createIndex('posts', 'idx_posts_author', ['author_id']); // Create composite index await supabaseServer.schema().createIndex('posts', 'idx_posts_author_published', ['author_id', 'is_published'] ); // Create unique index await supabaseServer.schema().createIndex('users', 'idx_users_email_unique', ['email'], { unique: true }); // Create index with specific method await supabaseServer.schema().createIndex('posts', 'idx_posts_content_gin', ['content'], { method: 'gin' // For full-text search }); // Create partial index with condition await supabaseServer.schema().createIndex('posts', 'idx_posts_published', ['created_at'], { method: 'btree', // Note: Partial indexes require custom SQL - this would need additional RPC function }); // Drop index await supabaseServer.schema().dropIndex('idx_posts_author'); ``` ## 3. Advanced CRUD Operations ### 3.1 Reading Data (SELECT) ```typescript // Select all data const { data: allPosts, error } = await supabase.from('posts').select('*'); // Select specific columns const { data: titles } = await supabase.from('posts').select('id, title, created_at'); // Conditional selection const { data: publishedPosts } = await supabase .from('posts') .select('*') .eq('is_published', true); // Ordering results const { data: recentPosts } = await supabase .from('posts') .select('*') .order('created_at', { ascending: false }); // Complex filtering const { data: filteredPosts } = await supabase .from('posts') .select('*') .eq('author_id', 'user-uuid') .eq('is_published', true) .order('view_count', { ascending: false }); // OR conditions const { data: posts } = await supabase .from('posts') .select('*') .or('is_featured.eq.true,view_count.gte.1000'); // Complex queries with joins (using Supabase's embedded resources) const { data: postsWithAuthors } = await supabase .from('posts') .select(` id, title, content, view_count, created_at, users!posts_author_id_fkey ( id, name, email, avatar_url ), comments ( id, content, created_at ) `) .eq('is_published', true) .order('created_at', { ascending: false }); // Pagination const { data: paginatedPosts } = await supabase .from('posts') .select('*') .range(0, 9) // First 10 items .order('created_at', { ascending: false }); // Count with data const { data: posts, count } = await supabase .from('posts') .select('*', { count: 'exact' }) .eq('is_published', true); // Text search const { data: searchResults } = await supabase .from('posts') .select('*') .textSearch('title', 'javascript', { type: 'websearch' }); ``` ### 3.2 Creating Data (INSERT) ```typescript // Insert single record const { data: newPost, error } = await supabase .from('posts') .insert({ title: 'New Blog Post', content: 'This is the content of the post.', author_id: 'user-uuid', is_published: true }) .select() .single(); // Insert multiple records const { data: newPosts } = await supabase .from('posts') .insert([ { title: 'First Post', content: 'First content', author_id: 'user-uuid' }, { title: 'Second Post', content: 'Second content', author_id: 'user-uuid' } ]) .select(); // Insert with specific columns returned const { data: createdPost } = await supabase .from('posts') .insert({ title: 'Title', content: 'Content', author_id: 'user-uuid' }) .select('id, title, created_at') .single(); // Upsert (insert or update if exists) const { data: upsertedPost } = await supabase .from('posts') .upsert({ id: 'existing-uuid', title: 'Updated or New Title', content: 'Updated or new content' }) .select() .single(); // Insert with conflict resolution const { data } = await supabase .from('users') .insert({ email: 'user@example.com', name: 'John Doe' }) .onConflict('email') // If email conflicts, do nothing .select() .single(); ``` ### 3.3 Updating Data (UPDATE) ```typescript // Update specific record const { data: updatedPost, error } = await supabase .from('posts') .update({ title: 'Updated Title', content: 'Updated Content', updated_at: new Date().toISOString() }) .eq('id', 'post-uuid') .select() .single(); // Update multiple records const { data: updatedPosts } = await supabase .from('posts') .update({ is_published: true }) .eq('author_id', 'user-uuid') .select(); // Conditional updates with complex conditions const { data } = await supabase .from('posts') .update({ is_featured: true }) .gte('view_count', 1000) .eq('is_published', true) .select(); // Increment values const { data: post } = await supabase .from('posts') .update({ view_count: 'view_count + 1' }) // SQL expression .eq('id', 'post-uuid') .select('id, view_count') .single(); // Update with JSON operations const { data } = await supabase .from('posts') .update({ metadata: { tags: ['javascript', 'tutorial'], difficulty: 'beginner' } }) .eq('id', 'post-uuid') .select(); // Batch update with different conditions const updates = [ { id: 'post1', view_count: 100 }, { id: 'post2', view_count: 200 } ]; for (const update of updates) { await supabase .from('posts') .update({ view_count: update.view_count }) .eq('id', update.id); } ``` ### 3.4 Deleting Data (DELETE) ```typescript // Delete specific record const { error } = await supabase .from('posts') .delete() .eq('id', 'post-uuid'); // Delete multiple records with conditions const { error: deleteError } = await supabase .from('posts') .delete() .eq('is_published', false) .eq('author_id', 'user-uuid'); // Delete and return deleted data const { data: deletedPosts } = await supabase .from('posts') .delete() .eq('author_id', 'inactive-user-uuid') .select(); // Soft delete (update instead of delete) const { data } = await supabase .from('posts') .update({ deleted_at: new Date().toISOString(), is_published: false }) .eq('id', 'post-uuid') .select(); // Delete with complex conditions const { error } = await supabase .from('posts') .delete() .lt('view_count', 10) .eq('is_published', false) .lt('created_at', '2023-01-01'); ``` ## 4. Row Level Security (RLS) Management ### 4.1 RLS Status Management ```typescript // Enable RLS await supabaseServer.rls('posts').enableRLS(); // Disable RLS (Security Warning: Use with caution!) await supabaseServer.rls('posts').disableRLS(); // Check RLS status const { data: rlsStatus } = await supabaseServer.rls('posts').checkRLSStatus(); console.log(rlsStatus); // { enabled: true } ``` ### 4.2 Creating RLS Policies ```typescript // Basic policy creation await supabaseServer.rls('posts').createPolicy( 'posts_select_policy', 'SELECT', 'auth.uid() = author_id' ); // Public read policy (items where is_public = true) await supabaseServer.rls('posts').createPublicReadPolicy(); // Open access policy (Security Warning: Use with extreme caution!) await supabaseServer.rls('posts').createOpenPolicy(); // User-specific access policy await supabaseServer.rls('posts').createPolicy( 'posts_user_policy', 'ALL', 'auth.uid() = author_id OR is_public = true' ); // Complex policies with multiple conditions await supabaseServer.rls('posts').createPolicy( 'posts_advanced_read', 'SELECT', ` (auth.uid() = author_id) OR (is_published = true AND is_public = true) OR (auth.uid() IN (SELECT user_id FROM collaborators WHERE post_id = posts.id)) ` ); // Time-based policies await supabaseServer.rls('posts').createPolicy( 'posts_scheduled_publish', 'SELECT', 'is_published = true AND published_at <= now()' ); // Role-based policies await supabaseServer.rls('posts').createPolicy( 'posts_admin_access', 'ALL', ` auth.uid() = author_id OR auth.jwt() ->> 'role' = 'admin' OR auth.jwt() ->> 'role' = 'moderator' ` ); ``` ### 4.3 Managing RLS Policies ```typescript // List all policies for a table const { data: policies } = await supabaseServer.rls('posts').getPolicies(); console.log(policies); /* Output example: [ { name: 'posts_select_policy', operation: 'SELECT', condition: 'auth.uid() = author_id', enabled: true } ] */ // Drop specific policy await supabaseServer.rls('posts').dropPolicy('posts_select_policy'); // Drop all policies (use with caution) const { data: allPolicies } = await supabaseServer.rls('posts').getPolicies(); for (const policy of allPolicies) { await supabaseServer.rls('posts').dropPolicy(policy.name); } ``` ### 4.4 Testing Access Permissions (Browser) ```typescript // Test current user's table access permissions const { data: accessTest } = await supabase.rls('posts').testAccess(); console.log(accessTest); /* Output: { canAccess: true, status: 200, message: 'Access granted' } */ // Test access for different operations async function testTableAccess(tableName: string) { const tests = [ { operation: 'SELECT', test: () => supabase.from(tableName).select('*').limit(1) }, { operation: 'INSERT', test: () => supabase.from(tableName).insert({}).select() }, { operation: 'UPDATE', test: () => supabase.from(tableName).update({}).eq('id', 'test') }, { operation: 'DELETE', test: () => supabase.from(tableName).delete().eq('id', 'test') } ]; for (const { operation, test } of tests) { try { await test(); console.log(`${operation} on ${tableName}: ALLOWED`); } catch (error) { console.log(`${operation} on ${tableName}: DENIED - ${error.message}`); } } } ``` ## 5. Real-World Usage Examples ### 5.1 Complete Blog System Implementation ```typescript async function setupBlogSystem() { // 1. Create users table await supabaseServer.schema().createTable('users', [ { name: 'id', type: 'uuid', primaryKey: true, defaultValue: 'gen_random_uuid()' }, { name: 'email', type: 'text', nullable: false, unique: true }, { name: 'name', type: 'text', nullable: false }, { name: 'avatar_url', type: 'text', nullable: true }, { name: 'bio', type: 'text', nullable: true }, { name: 'role', type: 'text', defaultValue: 'user' } ], { enableRLS: true }); // 2. Create categories table await supabaseServer.schema().createTable('categories', [ { name: 'id', type: 'uuid', primaryKey: true, defaultValue: 'gen_random_uuid()' }, { name: 'name', type: 'text', nullable: false, unique: true }, { name: 'slug', type: 'text', nullable: false, unique: true }, { name: 'description', type: 'text', nullable: true } ], { enableRLS: true }); // 3. Create posts table await supabaseServer.schema().createTable('posts', [ { name: 'id', type: 'uuid', primaryKey: true, defaultValue: 'gen_random_uuid()' }, { name: 'title', type: 'text', nullable: false }, { name: 'slug', type: 'text', nullable: false, unique: true }, { name: 'content', type: 'text', nullable: false }, { name: 'excerpt', type: 'text', nullable: true }, { name: 'author_id', type: 'uuid', references: { table: 'users', column: 'id', onDelete: 'CASCADE' } }, { name: 'category_id', type: 'uuid', references: { table: 'categories', column: 'id', onDelete: 'SET NULL' } }, { name: 'is_published', type: 'boolean', defaultValue: false }, { name: 'published_at', type: 'timestamp', nullable: true }, { name: 'view_count', type: 'integer', defaultValue: 0 }, { name: 'tags', type: 'text[]', nullable: true }, { name: 'featured_image_url', type: 'text', nullable: true } ], { enableRLS: true }); // 4. Create comments table await supabaseServer.schema().createTable('comments', [ { name: 'id', type: 'uuid', primaryKey: true, defaultValue: 'gen_random_uuid()' }, { name: 'post_id', type: 'uuid', references: { table: 'posts', column: 'id', onDelete: 'CASCADE' } }, { name: 'user_id', type: 'uuid', references: { table: 'users', column: 'id', onDelete: 'CASCADE' } }, { name: 'content', type: 'text', nullable: false }, { name: 'parent_id', type: 'uuid', references: { table: 'comments', column: 'id', onDelete: 'CASCADE' } }, { name: 'is_approved', type: 'boolean', defaultValue: false } ], { enableRLS: true }); // 5. Create performance indexes const indexes = [ { table: 'posts', name: 'idx_posts_author', columns: ['author_id'] }, { table: 'posts', name: 'idx_posts_published', columns: ['is_published', 'published_at'] }, { table: 'posts', name: 'idx_posts_category', columns: ['category_id'] }, { table: 'posts', name: 'idx_posts_slug', columns: ['slug'], options: { unique: true } }, { table: 'comments', name: 'idx_comments_post', columns: ['post_id'] }, { table: 'comments', name: 'idx_comments_user', columns: ['user_id'] }, { table: 'users', name: 'idx_users_email', columns: ['email'], options: { unique: true } }, { table: 'categories', name: 'idx_categories_slug', columns: ['slug'], options: { unique: true } } ]; for (const index of indexes) { await supabaseServer.schema().createIndex( index.table, index.name, index.columns, index.options ); } // 6. Set up RLS policies // Users policies await supabaseServer.rls('users').createPolicy( 'users_read_public', 'SELECT', 'true' // Users profiles are public ); await supabaseServer.rls('users').createPolicy( 'users_update_own', 'UPDATE', 'auth.uid() = id' ); // Categories policies (public read) await supabaseServer.rls('categories').createPolicy( 'categories_read_public', 'SELECT', 'true' ); // Posts policies await supabaseServer.rls('posts').createPolicy( 'posts_read_published', 'SELECT', 'is_published = true AND published_at <= now()' ); await supabaseServer.rls('posts').createPolicy( 'posts_author_full_access', 'ALL', 'auth.uid() = author_id' ); await supabaseServer.rls('posts').createPolicy( 'posts_admin_access', 'ALL', `auth.jwt() ->> 'role' IN ('admin', 'moderator')` ); // Comments policies await supabaseServer.rls('comments').createPolicy( 'comments_read_approved', 'SELECT', 'is_approved = true' ); await supabaseServer.rls('comments').createPolicy( 'comments_user_crud', 'ALL', 'auth.uid() = user_id' ); console.log('Blog system setup completed successfully!'); } ``` ### 5.2 Blog Operations ```typescript // Create a new blog post async function createBlogPost(postData: { title: string; content: string; excerpt?: string; categoryId?: string; tags?: string[]; featuredImageUrl?: string; }) { // Generate slug from title const slug = postData.title .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/(^-|-$)/g, ''); const { data: newPost, error } = await supabase .from('posts') .insert({ title: postData.title, slug: slug, content: postData.content, excerpt: postData.excerpt, category_id: postData.categoryId, tags: postData.tags, featured_image_url: postData.featuredImageUrl, is_published: false, // Draft by default author_id: 'current-user-id' // Get from auth context }) .select(` id, title, slug, content, is_published, created_at, categories ( name, slug ) `) .single(); return { data: newPost, error }; } // Publish a post async function publishPost(postId: string) { const { data, error } = await supabase .from('posts') .update({ is_published: true, published_at: new Date().toISOString() }) .eq('id', postId) .eq('author_id', 'current-user-id') // Ensure user owns the post .select() .single(); return { data, error }; } // Get published posts with pagination async function getPublishedPosts(page: number = 1, limit: number = 10) { const offset = (page - 1) * limit; const { data: posts, error, count } = await supabase .from('posts') .select(` id, title, slug, excerpt, view_count, published_at, featured_image_url, tags, users!posts_author_id_fkey ( id, name, avatar_url ), categories ( name, slug ) `, { count: 'exact' }) .eq('is_published', true) .lte('published_at', new Date().toISOString()) .order('published_at', { ascending: false }) .range(offset, offset + limit - 1); return { data: posts, error, totalCount: count, totalPages: count ? Math.ceil(count / limit) : 0, currentPage: page }; } // Get single post with comments async function getPostWithComments(slug: string) { const { data: post, error } = await supabase .from('posts') .select(` id, title, slug, content, view_count, published_at, tags, featured_image_url, users!posts_author_id_fkey ( id, name, avatar_url, bio ), categories ( name, slug ), comments!comments_post_id_fkey ( id, content, created_at, users!comments_user_id_fkey ( name, avatar_url ) ) `) .eq('slug', slug) .eq('is_published', true) .single(); if (post) { // Increment view count await supabase .from('posts') .update({ view_count: post.view_count + 1 }) .eq('id', post.id); } return { data: post, error }; } // Add comment to post async function addComment(postId: string, content: string, parentId?: string) { const { data: comment, error } = await supabase .from('comments') .insert({ post_id: postId, content: content, parent_id: parentId, user_id: 'current-user-id', // Get from auth context is_approved: false // Requires moderation }) .select(` id, content, created_at, users!comments_user_id_fkey ( name, avatar_url ) `) .single(); return { data: comment, error }; } // Search posts async function searchPosts(query: string, categoryId?: string) { let queryBuilder = supabase .from('posts') .select(` id, title, slug, excerpt, published_at, tags, users!posts_author_id_fkey ( name ), categories ( name, slug ) `) .eq('is_published', true) .textSearch('title', query, { type: 'websearch' }); if (categoryId) { queryBuilder = queryBuilder.eq('category_id', categoryId); } const { data: posts, error } = await queryBuilder .order('published_at', { ascending: false }); return { data: posts, error }; } ``` ### 5.3 E-commerce System Implementation ```typescript async function setupEcommerceSystem() { // 1. Create products table await supabaseServer.schema().createTable('products', [ { name: 'id', type: 'uuid', primaryKey: true, defaultValue: 'gen_random_uuid()' }, { name: 'name', type: 'text', nullable: false }, { name: 'slug', type: 'text', nullable: false, unique: true }, { name: 'description', type: 'text', nullable: true }, { name: 'price', type: 'decimal(10,2)', nullable: false }, { name: 'compare_at_price', type: 'decimal(10,2)', nullable: true }, { name: 'cost', type: 'decimal(10,2)', nullable: true }, { name: 'sku', type: 'text', unique: true, nullable: true }, { name: 'inventory_quantity', type: 'integer', defaultValue: 0 }, { name: 'track_inventory', type: 'boolean', defaultValue: true }, { name: 'weight', type: 'decimal(8,2)', nullable: true }, { name: 'is_active', type: 'boolean', defaultValue: true }, { name: 'featured_image_url', type: 'text', nullable: true }, { name: 'images', type: 'jsonb', defaultValue: '[]' }, { name: 'tags', type: 'text[]', nullable: true }, { name: 'seo_title', type: 'text', nullable: true }, { name: 'seo_description', type: 'text', nullable: true } ], { enableRLS: true }); // 2. Create product categories await supabaseServer.schema().createTable('product_categories', [ { name: 'id', type: 'uuid', primaryKey: true, defaultValue: 'gen_random_uuid()' }, { name: 'name', type: 'text', nullable: false }, { name: 'slug', type: 'text', nullable: false, unique: true }, { name: 'parent_id', type: 'uuid', references: { table: 'product_categories', column: 'id', onDelete: 'SET NULL' } }, { name: 'description', type: 'text', nullable: true }, { name: 'image_url', type: 'text', nullable: true }, { name: 'sort_order', type: 'integer', defaultValue: 0 } ], { enableRLS: true }); // 3. Create product-category junction table await supabaseServer.schema().createTable('product_category_relations', [ { name: 'product_id', type: 'uuid', references: { table: 'products', column: 'id', onDelete: 'CASCADE' } }, { name: 'category_id', type: 'uuid', references: { table: 'product_categories', column: 'id', onDelete: 'CASCADE' } } ], { enableRLS: true }); // Add composite primary key await supabaseServer.schema().createIndex( 'product_category_relations', 'product_category_relations_pkey', ['product_id', 'category_id'], { unique: true } ); // 4. Create orders table await supabaseServer.schema().createTable('orders', [ { name: 'id', type: 'uuid', primaryKey: true, defaultValue: 'gen_random_uuid()' }, { name: 'order_number', type: 'text', nullable: false, unique: true }, { name: 'user_id', type: 'uuid', references: { table: 'users', column: 'id', onDelete: 'SET NULL' } }, { name: 'email', type: 'text', nullable: false }, { name: 'status', type: 'text', defaultValue: 'pending' }, { name: 'subtotal', type: 'decimal(10,2)', nullable: false }, { name: 'tax_amount', type: 'decimal(10,2)', defaultValue: 0 }, { name: 'shipping_amount', type: 'decimal(10,2)', defaultValue: 0 }, { name: 'total_amount', type: 'decimal(10,2)', nullable: false }, { name: 'currency', type: 'text', defaultValue: 'USD' }, { name: 'shipping_address', type: 'jsonb', nullable: true }, { name: 'billing_address', type: 'jsonb', nullable: true }, { name: 'notes', type: 'text', nullable: true } ], { enableRLS: true }); // 5. Create order items table await supabaseServer.schema().createTable('order_items', [ { name: 'id', type: 'uuid', primaryKey: true, defaultValue: 'gen_random_uuid()' }, { name: 'order_id', type: 'uuid', references: { table: 'orders', column: 'id', onDelete: 'CASCADE' } }, { name: 'product_id', type: 'uuid', references: { table: 'products', column: 'id', onDelete: 'CASCADE' } }, { name: 'quantity', type: 'integer', nullable: false }, { name: 'unit_price', type: 'decimal(10,2)', nullable: false }, { name: 'total_price', type: 'decimal(10,2)', nullable: false }, { name: 'product_snapshot', type: 'jsonb', nullable: true // Store product details at time of purchase } ], { enableRLS: true }); // Create performance indexes for e-commerce const ecommerceIndexes = [ { table: 'products', name: 'idx_products_active', columns: ['is_active'] }, { table: 'products', name: 'idx_products_price', columns: ['price'] }, { table: 'products', name: 'idx_products_inventory', columns: ['inventory_quantity'] }, { table: 'products', name: 'idx_products_slug', columns: ['slug'], options: { unique: true } }, { table: 'products', name: 'idx_products_sku', columns: ['sku'], options: { unique: true } }, { table: 'product_categories', name: 'idx_categories_parent', columns: ['parent_id'] }, { table: 'product_categories', name: 'idx_categories_slug', columns: ['slug'], options: { unique: true } }, { table: 'orders', name: 'idx_orders_user', columns: ['user_id'] }, { table: 'orders', name: 'idx_orders_status', columns: ['status'] }, { table: 'orders', name: 'idx_orders_number', columns: ['order_number'], options: { unique: true } }, { table: 'order_items', name: 'idx_order_items_order', columns: ['order_id'] }, { table: 'order_items', name: 'idx_order_items_product', columns: ['product_id'] } ]; for (const index of ecommerceIndexes) { await supabaseServer.schema().createIndex( index.table, index.name, index.columns, index.options ); } // Set up RLS policies for e-commerce // Products - public read for active products await supabaseServer.rls('products').createPolicy( 'products_public_read', 'SELECT', 'is_active = true' ); // Categories - public read await supabaseServer.rls('product_categories').createPolicy( 'categories_public_read', 'SELECT', 'true' ); // Product-category relations - public read await supabaseServer.rls('product_category_relations').createPolicy( 'product_categories_public_read', 'SELECT', 'true' ); // Orders - users can only see their own orders await supabaseServer.rls('orders').createPolicy( 'orders_user_read', 'SELECT', 'auth.uid() = user_id' ); await supabaseServer.rls('orders').createPolicy( 'orders_user_create', 'INSERT', 'auth.uid() = user_id' ); // Order items - can read items from user's orders await supabaseServer.rls('order_items').createPolicy( 'order_items_user_read', 'SELECT', 'EXISTS (SELECT 1 FROM orders WHERE orders.id = order_items.order_id AND orders.user_id = auth.uid())' ); console.log('E-commerce system setup completed successfully!'); } // E-commerce operations async function getProductsByCategory(categorySlug: string, filters?: { minPrice?: number; maxPrice?: number; tags?: string[]; sortBy?: 'price_asc' | 'price_desc' | 'name' | 'created_at'; }) { let query = supabase .from('products') .select(` id, name, slug, description, price, compare_at_price, featured_image_url, tags, inventory_quantity, product_category_relations!inner ( product_categories!inner ( slug ) ) `) .eq('is_active', true) .eq('product_category_relations.product_categories.slug', categorySlug); // Apply filters if (filters?.minPrice) { query = query.gte('price', filters.minPrice); } if (filters?.maxPrice) { query = query.lte('price', filters.maxPrice); } if (filters?.tags && filters.tags.length > 0) { query = query.overlaps('tags', filters.tags); } // Apply sorting switch (filters?.sortBy) { case 'price_asc': query = query.order('price', { ascending: true }); break; case 'price_desc': query = query.order('price', { ascending: false }); break; case 'name': query = query.order('name', { ascending: true }); break; default: query = query.order('created_at', { ascending: false }); } const { data: products, error } = await query; return { data: products, error }; } async function createOrder(orderData: { items: Array<{ productId: string; quantity: number; }>; shippingAddress: any; billingAddress?: any; notes?: string; }) { // First, get product details and calculate totals const productIds = orderData.items.map(item => item.productId); const { data: products, error: productsError } = await supabase .from('products') .select('id, name, price, inventory_quantity') .in('id', productIds); if (productsError || !products) { return { data: null, error: productsError }; } // Check inventory and calculate totals let subtotal = 0; const orderItems = []; for (const item of orderData.items) { const product = products.find(p => p.id === item.productId); if (!product) { return { data: null, error: new Error(`Product ${item.productId} not found`) }; } if (product.inventory_quantity < item.quantity) { return { data: null, error: new Error(`Insufficient inventory for ${product.name}`) }; } const totalPrice = product.price * item.quantity; subtotal += totalPrice; orderItems.push({ product_id: item.productId, quantity: item.quantity, unit_price: product.price, total_price: totalPrice, product_snapshot: { name: product.name, price: product.price } }); } // Calculate tax and shipping (simplified) const taxAmount = subtotal * 0.1; // 10% tax const shippingAmount = subtotal > 100 ? 0 : 10; // Free shipping over $100 const totalAmount = subtotal + taxAmount + shippingAmount; // Generate order number const orderNumber = `ORD-${Date.now()}`; // Create order const { data: order, error: orderError } = await supabase .from('orders') .insert({ order_number: orderNumber, user_id: 'current-user-id', // Get from auth context email: 'user@example.com', // Get from user profile subtotal: subtotal, tax_amount: taxAmount, shipping_amount: shippingAmount, total_amount: totalAmount, shipping_address: orderData.shippingAddress, billing_address: orderData.billingAddress || orderData.shippingAddress, notes: orderData.notes, status: 'pending' }) .select() .single(); if (orderError) { return { data: null, error: orderError }; } // Create order items const orderItemsWithOrderId = orderItems.map(item => ({ ...item, order_id: order.id })); const { error: itemsError } = await supabase .from('order_items') .insert(orderItemsWithOrderId); if (itemsError) { // Rollback order creation if items fail await supabase.from('orders').delete().eq('id', order.id); return { data: null, error: itemsError }; } // Update inventory for (const item of orderData.items) { await supabase .from('products') .update({ inventory_quantity: `inventory_quantity - ${item.quantity}` }) .eq('id', item.productId); } return { data: order, error: null }; } ``` ## 6. Database Migration and Maintenance ### 6.1 Schema Migration Example ```typescript interface Migration { version: string; description: string; up: () => Promise<void>; down: () => Promise<void>; } const migrations: Migration[] = [ { version: '001', description: 'Add user profiles table', up: async () => { await supabaseServer.schema().createTable('user_profiles', [ { name: 'user_id', type: 'uuid', primaryKey: true, references: { table: 'auth.users', column: 'id', onDelete: 'CASCADE' } }, { name: 'first_name', type: 'text', nullable: true }, { name: 'last_name', type: 'text', nullable: true }, { name: 'phone', type: 'text', nullable: true } ], { enableRLS: true }); await supabaseServer.rls('user_profiles').createPolicy( 'user_profiles_own_access', 'ALL', 'auth.uid() = user_id' ); }, down: async () => { await supabaseServer.schema().dropTable('user_profiles'); } }, { version: '002', description: 'Add blog post slugs', up: async () => { await supabaseServer.schema().addColumn('posts', { name: 'slug', type: 'text', unique: true, nullable: true }); await supabaseServer.schema().createIndex('posts', 'idx_posts_slug', ['slug'], { unique: true }); }, down: async () => { await supabaseServer.schema().dropIndex('idx_posts_slug'); await supabaseServer.schema().dropColumn('posts', 'slug'); } } ]; async function runMigrations() { // Create migrations table if it doesn't exist const { data: exists } = await supabaseServer.schema().tableExists('migrations'); if (!exists) { await supabaseServer.schema().createTable('migrations', [ { name: 'version', type: 'text', primaryKey: true }, { name: 'description', type: 'text', nullable: false }, { name: 'executed_at', type: 'timestamp', defaultValue: 'now()' } ]); } // Get executed migrations const { data: executedMigrations } = await supabase .from('migrations') .select('version'); const executedVersions = new Set( executedMigrations?.map(m => m.version) || [] ); // Run pending migrations for (const migration of migrations) { if (!executedVersions.has(migration.version)) { try { console.log(`Running migration ${migration.version}: ${migration.description}`); await migration.up(); // Record migration as executed await supabase.from('migrations').insert({ version: migration.version, description: migration.description }); console.log(`Migration ${migration.version} completed successfully`); } catch (error) { console.error(`Migration ${migration.version} failed:`, error); // Optionally run rollback try { await migration.down(); } catch (rollbackError) { console.error(`Rollback failed for ${migration.version}:`, rollbackError); } break; } } } } ``` ### 6.2 Data Backup and Restore ```typescript async function backupTable(tableName: string, includeData: boolean = true) { // Get table structure const { data: tableInfo } = await supabaseServer.schema().getTableInfo(tableName); if (!tableInfo) { throw new Error(`Table ${tableName} not found`); } const backup = { tableName, structure: tableInfo, data: null as any[], createdAt: new Date().toISOString() }; if (includeData) { // Get all data from table const { data: tableData } = await supabase .from(tableName) .select('*'); backup.data = tableData || []; } // Save backup to file or storage const backupJson = JSON.stringify(backup, null, 2); // In a real application, you would save this to file system or cloud storage console.log(`Backup created for ${tableName}:`, backupJson); return backup; } async function restoreTable(backup: any) { const { tableName, structure, data } = backup; // Check if table exists const { data: exists } = await supabaseServer.schema().tableExists(tableName); if (!exists) { // Recreate table structure const columns = structure.columns.map((col: any) => ({ name: col.name, type: col.type, nullable: col.nullable, primaryKey: col.primaryKey, unique: col.unique, defaultValue: col.defaultValue, references: col.references })); await supabaseServer.schema().createTable(tableName, columns); } if (data && data.length > 0) { // Insert data in batches const batchSize = 100; for (let i = 0; i < data.length; i += batchSize) { const batch = data.slice(i, i + batchSize); await supabase.from(tableName).insert(batch); } } console.log(`Table ${tableName} restored successfully`); } ``` ### 6.3 Performance Monitoring ```typescript async function analyzeTablePerformance(tableName: string) { // Get table statistics (would need custom RPC functions) const { data: stats } = await supabaseServer.executeRPC('get_table_stats', { table_name: tableName }); // Analyze slow queries (would need custom logging) const { data: slowQueries } = await supabaseServer.executeRPC('get_slow_queries', { table_name: tableName, min_duration: 1000 // queries slower than 1 second }); // Check index usage const { data: indexStats } = await supabaseServer.executeRPC('get_index_usage', { table_name: tableName }); return { tableStats: stats, slowQueries: slowQueries, indexUsage: indexStats }; } async function optimizeTable(tableName: string) { const performance = await analyzeTablePerformance(tableName); const recommendations = []; // Check for missing indexes if (performance.slowQueries) { for (const query of performance.slowQueries) { if (query.type === 'seq_scan') { recommendations.push({ type: 'add_index', message: `Consider adding index on columns: ${query.columns.join(', ')}`, sql: `CREATE INDEX idx_${tableName}_${query.columns.join('_')} ON ${tableName} (${query.columns.join(', ')});` }); } } } // Check for unused indexes if (performance.indexUsage) { for (const index of performance.indexUsage) { if (index.usage_count < 10) { recommendations.push({ type: 'remove_index', message: `Index ${index.name} is rarely used, consider removing it`, sql: `DROP INDEX ${index.name};` }); } } } return recommendations; } ``` ## 7. Advanced Error Handling and Logging ```typescript interface DatabaseError extends Error { code?: string; details?: string; hint?: string; } class DatabaseOperations { private static logError(operation: string, error: DatabaseError, context?: any) { console.error(`Database Error in ${operation}:`, { message: error.message, code: error.code, details: error.details, hint: error.hint, context: context, timestamp: new Date().toISOString() }); } static async safeExecute<T>( operation: string, fn: () => Promise<{ data: T | null; error: any }>, context?: any ): Promise<{ data: T | null; error: DatabaseError | null }> { try { const result = await fn(); if (result.error) { this.logError(operation, result.error, context); return { data: null, error: result.error }; } return result; } catch (error) { const dbError = error as DatabaseError; this.logError(operation, dbError, context); return { data: null, error: dbError }; } } static async retryOperation<T>( operation: string, fn: () => Promise<{ data: T | null; error: any }>, maxRetries: number = 3, delay: number = 1000 ): Promise<{ data: T | null; error: DatabaseError | null }> { let lastError: DatabaseError | null = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { const result = await this.safeExecute(operation, fn, { attempt }); if (!result.error) { return result; } lastError = result.error; // Don't retry for certain error types if (result.error.code === '23505') { // unique_violation break; } if (attempt < maxRetries) { await new Promise(resolve => setTimeout(resolve, delay * attempt)); } } return { data: null, error: lastError }; } } // Usage examples async function createPostWithErrorHandling(postData: any) { return DatabaseOperations.safeExecute( 'create_post', () => supabase.from('posts').insert(postData).select().single(), { postData } ); } async function createPostWithRetry(postData: any) { return DatabaseOperations.retryOperation( 'create_post_retry', () => supabase.from('posts').insert(postData).select().single(), 3, // max retries 1000 // base delay ); } ``` ## 8. Security Best Practices ### 8.1 Environment Configuration ```typescript // config/database.ts interface DatabaseConfig { url: string; anonKey: string; serviceRoleKey?: string; environment: 'development' | 'staging' | 'production'; } const config: DatabaseConfig = { url: process.env.SUPABASE_URL || '', anonKey: process.env.SUPABASE_ANON_KEY || '', serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY, // Server only! environment: (process.env.NODE_ENV as any) || 'development' }; // Never expose service role key to browser export const getBrowserClient = () => { return new SupabaseClient(config.url, config.anonKey); }; export const getServerClient = () => { if (!config.serviceRoleKey) { throw new Error('Service role key not configured'); } return new SupabaseServer(config.url, config.anonKey, config.serviceRoleKey); }; ``` ### 8.2 Input Validation and Sanitization ```typescript import { z } from 'zod'; // Define schemas for data validation const createPostSchema = z.object({ title: z.string().min(1).max(200), content: z.string().min(10), author_id: z.string().uuid(), is_published: z.boolean().default(false), tags: z.array(z.string()).optional(), category_id: z.string().uuid().optional() }); async function createValidatedPost(rawData: any) { try { // Validate input data const validatedData = createPostSchema.parse(rawData); // Sanitize content (remove dangerous HTML, etc.) const sanitizedData = { ...validatedData, content: sanitizeHtml(validatedData.content), title: validatedData.title.trim() }; // Create post const { data, error } = await supabase .from('posts') .insert(sanitizedData) .select() .single(); return { data, error }; } catch (error) { if (error instanceof z.ZodError) { return { data: null, error: { message: 'Validation failed', details: error.errors } }; } throw error; } } function sanitizeHtml(html: string): string { // Implement HTML sanitization // This is a simplified example - use a proper library like DOMPurify return html .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') .replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, ''); } ``` ## 9. Testing ```typescript // tests/database.t