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:
Giancarlo Buomprisco
2025-11-01 11:59:52 +07:00
committed by GitHub
parent a920dea2b3
commit 116d41a284
73 changed files with 5638 additions and 558 deletions

View 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

View 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;
```

View 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.

View 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

View File

@@ -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.

View 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)