Changelog (#399)
* feat: add changelog feature and update site navigation - Introduced a new Changelog page with pagination and detailed entry views. - Added components for displaying changelog entries, pagination, and entry details. - Updated site navigation to include a link to the new Changelog page. - Enhanced localization for changelog-related texts in marketing.json. * refactor: enhance Changelog page layout and entry display - Increased the number of changelog entries displayed per page from 2 to 20 for better visibility. - Improved the layout of the Changelog page by adjusting the container styles and removing unnecessary divs. - Updated the ChangelogEntry component to enhance the visual presentation of entries, including a new date badge with an icon. - Refined the CSS styles for Markdoc headings to improve typography and spacing. * refactor: enhance Changelog page functionality and layout - Increased the number of changelog entries displayed per page from 20 to 50 for improved user experience. - Updated ChangelogEntry component to make the highlight prop optional and refined the layout for better visual clarity. - Adjusted styles in ChangelogHeader and ChangelogPagination components for a more cohesive design. - Removed unnecessary order fields from changelog markdown files to streamline content management. * feat: enhance Changelog entry navigation and data loading - Refactored ChangelogEntry page to load previous and next entries for improved navigation. - Introduced ChangelogNavigation component to facilitate navigation between changelog entries. - Updated ChangelogDetail component to display navigation links and entry details. - Enhanced data fetching logic to retrieve all changelog entries alongside the current entry. - Added localization keys for navigation text in marketing.json. * Update package dependencies and enhance documentation layout - Upgraded various packages including @turbo/gen and turbo to version 2.6.0, and react-hook-form to version 7.66.0. - Updated lucide-react to version 0.552.0 across multiple packages. - Refactored documentation layout components for improved styling and structure. - Removed deprecated loading components and adjusted navigation elements for better user experience. - Added placeholder notes in changelog entries for clarity.
This commit is contained in:
committed by
GitHub
parent
a920dea2b3
commit
116d41a284
16
apps/web/content/documentation/database/database.mdoc
Normal file
16
apps/web/content/documentation/database/database.mdoc
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: "Database"
|
||||
description: "Learn how to work with the Supabase database in your MakerKit application."
|
||||
publishedAt: 2024-04-11
|
||||
order: 2
|
||||
status: "published"
|
||||
---
|
||||
|
||||
MakerKit uses Supabase Postgres for database management with built-in security and performance.
|
||||
|
||||
This section covers:
|
||||
- Database schema and structure
|
||||
- Running migrations
|
||||
- Row Level Security (RLS)
|
||||
- Querying data
|
||||
- Functions and triggers
|
||||
446
apps/web/content/documentation/database/functions-triggers.mdoc
Normal file
446
apps/web/content/documentation/database/functions-triggers.mdoc
Normal file
@@ -0,0 +1,446 @@
|
||||
---
|
||||
title: "Functions & Triggers"
|
||||
description: "Create database functions and triggers for automated logic."
|
||||
publishedAt: 2024-04-11
|
||||
order: 4
|
||||
status: "published"
|
||||
---
|
||||
|
||||
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||
|
||||
Database functions and triggers enable server-side logic and automation.
|
||||
|
||||
## Database Functions
|
||||
|
||||
### Creating a Function
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION get_user_projects(user_id UUID)
|
||||
RETURNS TABLE (
|
||||
id UUID,
|
||||
name TEXT,
|
||||
created_at TIMESTAMPTZ
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT p.id, p.name, p.created_at
|
||||
FROM projects p
|
||||
INNER JOIN accounts_memberships am ON am.account_id = p.account_id
|
||||
WHERE am.user_id = get_user_projects.user_id;
|
||||
END;
|
||||
$$;
|
||||
```
|
||||
|
||||
### Calling from TypeScript
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client.rpc('get_user_projects', {
|
||||
user_id: userId,
|
||||
});
|
||||
```
|
||||
|
||||
## Common Function Patterns
|
||||
|
||||
### Get User Accounts
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION get_user_accounts(user_id UUID)
|
||||
RETURNS TABLE (account_id UUID)
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
SELECT account_id
|
||||
FROM accounts_memberships
|
||||
WHERE user_id = $1;
|
||||
$$;
|
||||
```
|
||||
|
||||
### Check Permission
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION has_permission(
|
||||
user_id UUID,
|
||||
account_id UUID,
|
||||
required_role TEXT
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
user_role TEXT;
|
||||
BEGIN
|
||||
SELECT role INTO user_role
|
||||
FROM accounts_memberships
|
||||
WHERE user_id = has_permission.user_id
|
||||
AND account_id = has_permission.account_id;
|
||||
|
||||
RETURN user_role = required_role OR user_role = 'owner';
|
||||
END;
|
||||
$$;
|
||||
```
|
||||
|
||||
### Search Function
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION search_projects(
|
||||
search_term TEXT,
|
||||
account_id UUID
|
||||
)
|
||||
RETURNS TABLE (
|
||||
id UUID,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
relevance REAL
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
p.description,
|
||||
ts_rank(
|
||||
to_tsvector('english', p.name || ' ' || COALESCE(p.description, '')),
|
||||
plainto_tsquery('english', search_term)
|
||||
) AS relevance
|
||||
FROM projects p
|
||||
WHERE p.account_id = search_projects.account_id
|
||||
AND (
|
||||
to_tsvector('english', p.name || ' ' || COALESCE(p.description, ''))
|
||||
@@ plainto_tsquery('english', search_term)
|
||||
)
|
||||
ORDER BY relevance DESC;
|
||||
END;
|
||||
$$;
|
||||
```
|
||||
|
||||
## Triggers
|
||||
|
||||
### Auto-Update Timestamp
|
||||
|
||||
```sql
|
||||
-- Create trigger function
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Attach to table
|
||||
CREATE TRIGGER update_projects_updated_at
|
||||
BEFORE UPDATE ON projects
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
```
|
||||
|
||||
### Audit Log Trigger
|
||||
|
||||
```sql
|
||||
-- Create audit log table
|
||||
CREATE TABLE audit_log (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
table_name TEXT NOT NULL,
|
||||
record_id UUID NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
old_data JSONB,
|
||||
new_data JSONB,
|
||||
user_id UUID,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create trigger function
|
||||
CREATE OR REPLACE FUNCTION log_changes()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
INSERT INTO audit_log (table_name, record_id, action, new_data, user_id)
|
||||
VALUES (TG_TABLE_NAME, NEW.id, 'INSERT', to_jsonb(NEW), auth.uid());
|
||||
RETURN NEW;
|
||||
ELSIF TG_OP = 'UPDATE' THEN
|
||||
INSERT INTO audit_log (table_name, record_id, action, old_data, new_data, user_id)
|
||||
VALUES (TG_TABLE_NAME, NEW.id, 'UPDATE', to_jsonb(OLD), to_jsonb(NEW), auth.uid());
|
||||
RETURN NEW;
|
||||
ELSIF TG_OP = 'DELETE' THEN
|
||||
INSERT INTO audit_log (table_name, record_id, action, old_data, user_id)
|
||||
VALUES (TG_TABLE_NAME, OLD.id, 'DELETE', to_jsonb(OLD), auth.uid());
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Attach to table
|
||||
CREATE TRIGGER audit_projects
|
||||
AFTER INSERT OR UPDATE OR DELETE ON projects
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION log_changes();
|
||||
```
|
||||
|
||||
### Cascade Soft Delete
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION soft_delete_cascade()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
-- Soft delete related tasks
|
||||
UPDATE tasks
|
||||
SET deleted_at = NOW()
|
||||
WHERE project_id = OLD.id
|
||||
AND deleted_at IS NULL;
|
||||
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER soft_delete_project_tasks
|
||||
BEFORE DELETE ON projects
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION soft_delete_cascade();
|
||||
```
|
||||
|
||||
## Validation Triggers
|
||||
|
||||
### Enforce Business Rules
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION validate_project_budget()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NEW.budget < 0 THEN
|
||||
RAISE EXCEPTION 'Budget cannot be negative';
|
||||
END IF;
|
||||
|
||||
IF NEW.budget > 1000000 THEN
|
||||
RAISE EXCEPTION 'Budget cannot exceed 1,000,000';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER check_project_budget
|
||||
BEFORE INSERT OR UPDATE ON projects
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION validate_project_budget();
|
||||
```
|
||||
|
||||
### Prevent Orphaned Records
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION prevent_owner_removal()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
owner_count INTEGER;
|
||||
BEGIN
|
||||
IF OLD.role = 'owner' THEN
|
||||
SELECT COUNT(*) INTO owner_count
|
||||
FROM accounts_memberships
|
||||
WHERE account_id = OLD.account_id
|
||||
AND role = 'owner'
|
||||
AND id != OLD.id;
|
||||
|
||||
IF owner_count = 0 THEN
|
||||
RAISE EXCEPTION 'Cannot remove the last owner of an account';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER check_owner_before_delete
|
||||
BEFORE DELETE ON accounts_memberships
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION prevent_owner_removal();
|
||||
```
|
||||
|
||||
## Computed Columns
|
||||
|
||||
### Virtual Column with Function
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION project_task_count(project_id UUID)
|
||||
RETURNS INTEGER
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
AS $$
|
||||
SELECT COUNT(*)::INTEGER
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND deleted_at IS NULL;
|
||||
$$;
|
||||
|
||||
-- Use in queries
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
project_task_count(id) as task_count
|
||||
FROM projects;
|
||||
```
|
||||
|
||||
## Event Notifications
|
||||
|
||||
### Notify on Changes
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION notify_project_change()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify(
|
||||
'project_changes',
|
||||
json_build_object(
|
||||
'operation', TG_OP,
|
||||
'record', NEW
|
||||
)::text
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER project_change_notification
|
||||
AFTER INSERT OR UPDATE ON projects
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_project_change();
|
||||
```
|
||||
|
||||
### Listen in TypeScript
|
||||
|
||||
```typescript
|
||||
const channel = client
|
||||
.channel('project_changes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'projects',
|
||||
},
|
||||
(payload) => {
|
||||
console.log('Project changed:', payload);
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
```
|
||||
|
||||
## Security Functions
|
||||
|
||||
### Row Level Security Helper
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION is_account_member(account_id UUID)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
STABLE
|
||||
AS $$
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM accounts_memberships
|
||||
WHERE account_id = $1
|
||||
AND user_id = auth.uid()
|
||||
);
|
||||
$$;
|
||||
|
||||
-- Use in RLS policy
|
||||
CREATE POLICY "Users can access their account's projects"
|
||||
ON projects FOR ALL
|
||||
USING (is_account_member(account_id));
|
||||
```
|
||||
|
||||
## Scheduled Functions
|
||||
|
||||
### Using pg_cron Extension
|
||||
|
||||
```sql
|
||||
-- Enable pg_cron extension
|
||||
CREATE EXTENSION IF NOT EXISTS pg_cron;
|
||||
|
||||
-- Schedule cleanup job
|
||||
SELECT cron.schedule(
|
||||
'cleanup-old-sessions',
|
||||
'0 2 * * *', -- Every day at 2 AM
|
||||
$$
|
||||
DELETE FROM sessions
|
||||
WHERE expires_at < NOW();
|
||||
$$
|
||||
);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use SECURITY DEFINER carefully** - Can bypass RLS
|
||||
2. **Add error handling** - Use EXCEPTION blocks
|
||||
3. **Keep functions simple** - One responsibility per function
|
||||
4. **Document functions** - Add comments
|
||||
5. **Test thoroughly** - Unit test database functions
|
||||
6. **Use STABLE/IMMUTABLE** - Performance optimization
|
||||
7. **Avoid side effects** - Make functions predictable
|
||||
8. **Return proper types** - Use RETURNS TABLE for clarity
|
||||
|
||||
## Testing Functions
|
||||
|
||||
```sql
|
||||
-- Test function
|
||||
DO $$
|
||||
DECLARE
|
||||
result INTEGER;
|
||||
BEGIN
|
||||
SELECT project_task_count('some-uuid') INTO result;
|
||||
|
||||
ASSERT result >= 0, 'Task count should not be negative';
|
||||
|
||||
RAISE NOTICE 'Test passed: task count = %', result;
|
||||
END;
|
||||
$$;
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Enable Function Logging
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION debug_function()
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'Debug: Processing started';
|
||||
RAISE NOTICE 'Debug: Current user is %', auth.uid();
|
||||
-- Your function logic
|
||||
RAISE NOTICE 'Debug: Processing completed';
|
||||
END;
|
||||
$$;
|
||||
```
|
||||
|
||||
### Check Function Execution
|
||||
|
||||
```sql
|
||||
-- View function execution stats
|
||||
SELECT
|
||||
schemaname,
|
||||
funcname,
|
||||
calls,
|
||||
total_time,
|
||||
self_time
|
||||
FROM pg_stat_user_functions
|
||||
ORDER BY total_time DESC;
|
||||
```
|
||||
68
apps/web/content/documentation/database/migrations.mdoc
Normal file
68
apps/web/content/documentation/database/migrations.mdoc
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: "Migrations"
|
||||
description: "Learn how to create and manage database migrations in your application."
|
||||
publishedAt: 2024-04-11
|
||||
order: 1
|
||||
status: "published"
|
||||
---
|
||||
|
||||
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||
|
||||
Database migrations allow you to version control your database schema changes and apply them consistently across environments.
|
||||
|
||||
## Creating a Migration
|
||||
|
||||
To create a new migration, use the following command:
|
||||
|
||||
```bash
|
||||
pnpm --filter web supabase:db:diff
|
||||
```
|
||||
|
||||
This will generate a new migration file in the `apps/web/supabase/migrations` directory based on the differences between your local database and the schema files.
|
||||
|
||||
## Applying Migrations
|
||||
|
||||
To apply migrations to your local database:
|
||||
|
||||
```bash
|
||||
pnpm --filter web supabase migrations up
|
||||
```
|
||||
|
||||
## Migration Best Practices
|
||||
|
||||
1. **Always test migrations locally first** before applying to production
|
||||
2. **Make migrations reversible** when possible by including DOWN statements
|
||||
3. **Use transactions** to ensure atomic operations
|
||||
4. **Add indexes** for foreign keys and frequently queried columns
|
||||
5. **Include RLS policies** in the same migration as table creation
|
||||
|
||||
## Example Migration
|
||||
|
||||
```sql
|
||||
-- Create a new table
|
||||
CREATE TABLE tasks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
completed BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- Add RLS
|
||||
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Create policies
|
||||
CREATE POLICY "Users can view their account tasks"
|
||||
ON tasks FOR SELECT
|
||||
USING (account_id IN (SELECT get_user_accounts(auth.uid())));
|
||||
```
|
||||
|
||||
## Resetting the Database
|
||||
|
||||
To completely reset your local database with the latest schema:
|
||||
|
||||
```bash
|
||||
pnpm supabase:web:reset
|
||||
```
|
||||
|
||||
This will drop all tables and reapply all migrations from scratch.
|
||||
430
apps/web/content/documentation/database/querying-data.mdoc
Normal file
430
apps/web/content/documentation/database/querying-data.mdoc
Normal file
@@ -0,0 +1,430 @@
|
||||
---
|
||||
title: "Querying Data"
|
||||
description: "Learn how to query and filter data from your database."
|
||||
publishedAt: 2024-04-11
|
||||
order: 3
|
||||
status: "published"
|
||||
---
|
||||
|
||||
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||
|
||||
Efficiently query and filter data using Supabase's query builder.
|
||||
|
||||
## Basic Queries
|
||||
|
||||
### Select All
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client
|
||||
.from('projects')
|
||||
.select('*');
|
||||
```
|
||||
|
||||
### Select Specific Columns
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client
|
||||
.from('projects')
|
||||
.select('id, name, created_at');
|
||||
```
|
||||
|
||||
### Select with Related Data
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client
|
||||
.from('projects')
|
||||
.select(`
|
||||
id,
|
||||
name,
|
||||
account:accounts(id, name),
|
||||
tasks(id, title, completed)
|
||||
`);
|
||||
```
|
||||
|
||||
## Filtering
|
||||
|
||||
### Equal
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('status', 'active');
|
||||
```
|
||||
|
||||
### Not Equal
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.neq('status', 'deleted');
|
||||
```
|
||||
|
||||
### Greater Than / Less Than
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.gt('created_at', '2024-01-01')
|
||||
.lt('budget', 10000);
|
||||
```
|
||||
|
||||
### In Array
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.in('status', ['active', 'pending']);
|
||||
```
|
||||
|
||||
### Like (Pattern Matching)
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.like('name', '%website%');
|
||||
```
|
||||
|
||||
### Full-Text Search
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.textSearch('description', 'design & development');
|
||||
```
|
||||
|
||||
## Ordering
|
||||
|
||||
### Order By
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
```
|
||||
|
||||
### Multiple Order By
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.order('status')
|
||||
.order('created_at', { ascending: false });
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
### Limit
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.limit(10);
|
||||
```
|
||||
|
||||
### Range (Offset)
|
||||
|
||||
```typescript
|
||||
const page = 2;
|
||||
const pageSize = 10;
|
||||
const from = (page - 1) * pageSize;
|
||||
const to = from + pageSize - 1;
|
||||
|
||||
const { data, count } = await client
|
||||
.from('projects')
|
||||
.select('*', { count: 'exact' })
|
||||
.range(from, to);
|
||||
```
|
||||
|
||||
## Aggregations
|
||||
|
||||
### Count
|
||||
|
||||
```typescript
|
||||
const { count } = await client
|
||||
.from('projects')
|
||||
.select('*', { count: 'exact', head: true });
|
||||
```
|
||||
|
||||
### Count with Filters
|
||||
|
||||
```typescript
|
||||
const { count } = await client
|
||||
.from('projects')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('status', 'active');
|
||||
```
|
||||
|
||||
## Advanced Queries
|
||||
|
||||
### Multiple Filters
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('account_id', accountId)
|
||||
.eq('status', 'active')
|
||||
.gte('created_at', startDate)
|
||||
.lte('created_at', endDate)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(20);
|
||||
```
|
||||
|
||||
### OR Conditions
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.or('status.eq.active,status.eq.pending');
|
||||
```
|
||||
|
||||
### Nested OR
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.or('and(status.eq.active,priority.eq.high),status.eq.urgent');
|
||||
```
|
||||
|
||||
## Joins
|
||||
|
||||
### Inner Join
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select(`
|
||||
*,
|
||||
account:accounts!inner(
|
||||
id,
|
||||
name
|
||||
)
|
||||
`)
|
||||
.eq('account.name', 'Acme Corp');
|
||||
```
|
||||
|
||||
### Left Join
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select(`
|
||||
*,
|
||||
tasks(*)
|
||||
`);
|
||||
```
|
||||
|
||||
## Null Handling
|
||||
|
||||
### Is Null
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.is('completed_at', null);
|
||||
```
|
||||
|
||||
### Not Null
|
||||
|
||||
```typescript
|
||||
const { data} = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.not('completed_at', 'is', null);
|
||||
```
|
||||
|
||||
## Insert Data
|
||||
|
||||
### Single Insert
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client
|
||||
.from('projects')
|
||||
.insert({
|
||||
name: 'New Project',
|
||||
account_id: accountId,
|
||||
status: 'active',
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
```
|
||||
|
||||
### Multiple Insert
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client
|
||||
.from('projects')
|
||||
.insert([
|
||||
{ name: 'Project 1', account_id: accountId },
|
||||
{ name: 'Project 2', account_id: accountId },
|
||||
])
|
||||
.select();
|
||||
```
|
||||
|
||||
## Update Data
|
||||
|
||||
### Update with Filter
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client
|
||||
.from('projects')
|
||||
.update({ status: 'completed' })
|
||||
.eq('id', projectId)
|
||||
.select()
|
||||
.single();
|
||||
```
|
||||
|
||||
### Update Multiple Rows
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client
|
||||
.from('projects')
|
||||
.update({ status: 'archived' })
|
||||
.eq('account_id', accountId)
|
||||
.lt('updated_at', oldDate);
|
||||
```
|
||||
|
||||
## Delete Data
|
||||
|
||||
### Delete with Filter
|
||||
|
||||
```typescript
|
||||
const { error } = await client
|
||||
.from('projects')
|
||||
.delete()
|
||||
.eq('id', projectId);
|
||||
```
|
||||
|
||||
### Delete Multiple
|
||||
|
||||
```typescript
|
||||
const { error } = await client
|
||||
.from('projects')
|
||||
.delete()
|
||||
.in('id', projectIds);
|
||||
```
|
||||
|
||||
## Upsert
|
||||
|
||||
### Insert or Update
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client
|
||||
.from('projects')
|
||||
.upsert({
|
||||
id: projectId,
|
||||
name: 'Updated Name',
|
||||
status: 'active',
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
```
|
||||
|
||||
## RPC (Stored Procedures)
|
||||
|
||||
### Call Database Function
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client
|
||||
.rpc('get_user_projects', {
|
||||
user_id: userId,
|
||||
});
|
||||
```
|
||||
|
||||
### With Complex Parameters
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client
|
||||
.rpc('search_projects', {
|
||||
search_term: 'design',
|
||||
account_ids: [1, 2, 3],
|
||||
min_budget: 5000,
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Basic Error Handling
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client
|
||||
.from('projects')
|
||||
.select('*');
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching projects:', error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
```
|
||||
|
||||
### Typed Error Handling
|
||||
|
||||
```typescript
|
||||
import { PostgrestError } from '@supabase/supabase-js';
|
||||
|
||||
function handleDatabaseError(error: PostgrestError) {
|
||||
switch (error.code) {
|
||||
case '23505': // unique_violation
|
||||
throw new Error('A project with this name already exists');
|
||||
case '23503': // foreign_key_violation
|
||||
throw new Error('Invalid account reference');
|
||||
default:
|
||||
throw new Error('Database error: ' + error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Types
|
||||
|
||||
### Generated Types
|
||||
|
||||
```typescript
|
||||
import { Database } from '~/types/database.types';
|
||||
|
||||
type Project = Database['public']['Tables']['projects']['Row'];
|
||||
type ProjectInsert = Database['public']['Tables']['projects']['Insert'];
|
||||
type ProjectUpdate = Database['public']['Tables']['projects']['Update'];
|
||||
```
|
||||
|
||||
### Typed Queries
|
||||
|
||||
```typescript
|
||||
const { data } = await client
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.returns<Project[]>();
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Select only needed columns** - Don't use `select('*')` unnecessarily
|
||||
2. **Use indexes** - Create indexes on frequently filtered columns
|
||||
3. **Limit results** - Always paginate large datasets
|
||||
4. **Avoid N+1 queries** - Use joins instead of multiple queries
|
||||
5. **Use RPC for complex queries** - Move logic to database
|
||||
6. **Cache when possible** - Use React Query or similar
|
||||
7. **Profile queries** - Use `EXPLAIN ANALYZE` in SQL
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always handle errors** - Check error responses
|
||||
2. **Validate input** - Use Zod or similar
|
||||
3. **Use TypeScript** - Generate and use types
|
||||
4. **Consistent naming** - Follow database naming conventions
|
||||
5. **Document complex queries** - Add comments
|
||||
6. **Test queries** - Unit test database operations
|
||||
7. **Monitor performance** - Track slow queries
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: "Row Level Security"
|
||||
description: "Understanding and implementing Row Level Security (RLS) for data protection."
|
||||
publishedAt: 2024-04-11
|
||||
order: 2
|
||||
status: "published"
|
||||
---
|
||||
|
||||
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||
|
||||
Row Level Security (RLS) is PostgreSQL's built-in authorization system that controls which rows users can access in database tables.
|
||||
|
||||
## Why RLS?
|
||||
|
||||
RLS provides several advantages:
|
||||
- **Database-level security** - Protection even if application code has bugs
|
||||
- **Automatic enforcement** - No need for manual authorization checks
|
||||
- **Multi-tenant isolation** - Ensures users only see their own data
|
||||
- **Performance** - Optimized at the database level
|
||||
|
||||
## Enabling RLS
|
||||
|
||||
All tables should have RLS enabled:
|
||||
|
||||
```sql
|
||||
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
|
||||
```
|
||||
|
||||
## Common Policy Patterns
|
||||
|
||||
### Personal Account Access
|
||||
|
||||
```sql
|
||||
CREATE POLICY "Users can access their personal account data"
|
||||
ON your_table FOR ALL
|
||||
USING (account_id = auth.uid());
|
||||
```
|
||||
|
||||
### Team Account Access
|
||||
|
||||
```sql
|
||||
CREATE POLICY "Users can access their team account data"
|
||||
ON your_table FOR ALL
|
||||
USING (
|
||||
account_id IN (
|
||||
SELECT account_id FROM accounts_memberships
|
||||
WHERE user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### Read vs Write Permissions
|
||||
|
||||
```sql
|
||||
-- All members can read
|
||||
CREATE POLICY "Team members can view data"
|
||||
ON your_table FOR SELECT
|
||||
USING (account_id IN (SELECT get_user_accounts(auth.uid())));
|
||||
|
||||
-- Only owners can modify
|
||||
CREATE POLICY "Only owners can modify data"
|
||||
ON your_table FOR UPDATE
|
||||
USING (
|
||||
account_id IN (
|
||||
SELECT account_id FROM accounts_memberships
|
||||
WHERE user_id = auth.uid() AND role = 'owner'
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## Testing RLS Policies
|
||||
|
||||
Always test your RLS policies to ensure they work correctly:
|
||||
|
||||
```sql
|
||||
-- Test as specific user
|
||||
SET request.jwt.claims.sub = 'user-uuid-here';
|
||||
|
||||
-- Try to select data
|
||||
SELECT * FROM your_table;
|
||||
|
||||
-- Reset
|
||||
RESET request.jwt.claims.sub;
|
||||
```
|
||||
|
||||
## Admin Bypass
|
||||
|
||||
Service role keys bypass RLS. Use with extreme caution and always implement manual authorization checks when using the admin client.
|
||||
43
apps/web/content/documentation/database/schema.mdoc
Normal file
43
apps/web/content/documentation/database/schema.mdoc
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: "Database Overview"
|
||||
description: "Understanding the database schema and table structure in your application."
|
||||
publishedAt: 2024-04-11
|
||||
order: 0
|
||||
status: "published"
|
||||
---
|
||||
|
||||
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||
|
||||
The database schema is designed with a multi-tenant architecture that supports both personal and team accounts.
|
||||
|
||||
## Core Tables
|
||||
|
||||
### Users Table
|
||||
The `users` table stores user authentication data and is managed by Supabase Auth:
|
||||
- `id` - Unique user identifier
|
||||
- `email` - User's email address
|
||||
- `created_at` - Account creation timestamp
|
||||
|
||||
### Accounts Table
|
||||
The `accounts` table represents both personal and team accounts:
|
||||
- `id` - Unique account identifier
|
||||
- `name` - Account display name
|
||||
- `slug` - URL-friendly identifier
|
||||
- `is_personal_account` - Boolean flag for personal vs team accounts
|
||||
|
||||
### Projects Table
|
||||
Store your application's project data:
|
||||
- `id` - Unique project identifier
|
||||
- `account_id` - Foreign key to accounts table
|
||||
- `name` - Project name
|
||||
- `description` - Project description
|
||||
- `created_at` - Creation timestamp
|
||||
|
||||
## Relationships
|
||||
|
||||
All data in the application is tied to accounts through foreign key relationships. This ensures proper data isolation and access control through Row Level Security (RLS).
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn about [migrations](/docs/database/migrations)
|
||||
- Understand [RLS policies](/docs/database/row-level-security)
|
||||
Reference in New Issue
Block a user