@divetocode/supa-query-builder
Version:
unofficial supabase query builder
2,128 lines (1,888 loc) • 69 kB
Markdown
# 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