* 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.
447 lines
8.3 KiB
Plaintext
447 lines
8.3 KiB
Plaintext
---
|
|
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;
|
|
```
|