Optimized agents rules subfolders, dependencies updates (#355)
* Update AGENTS.md and CLAUDE.md for improved clarity and structure * Added MCP Server * Added missing triggers to tables that should have used them * Updated all dependencies * Fixed rare bug in React present in the Admin layout which prevents navigating to pages (sometimes...)
This commit is contained in:
committed by
GitHub
parent
9fae142f2d
commit
533dfba5b9
264
apps/web/AGENTS.md
Normal file
264
apps/web/AGENTS.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# Web Application Instructions
|
||||
|
||||
This file contains instructions specific to the main Next.js web application.
|
||||
|
||||
## Application Structure
|
||||
|
||||
### Route Organization
|
||||
|
||||
```
|
||||
app/
|
||||
├── (marketing)/ # Public pages (landing, blog, docs)
|
||||
├── (auth)/ # Authentication pages
|
||||
├── home/
|
||||
│ ├── (user)/ # Personal account context
|
||||
│ └── [account]/ # Team account context ([account] = team slug)
|
||||
├── admin/ # Super admin section
|
||||
└── api/ # API routes
|
||||
```
|
||||
|
||||
Key Examples:
|
||||
|
||||
- Marketing layout: `app/(marketing)/layout.tsx`
|
||||
- Personal dashboard: `app/home/(user)/page.tsx`
|
||||
- Team workspace: `app/home/[account]/page.tsx`
|
||||
- Admin section: `app/admin/page.tsx`
|
||||
|
||||
### Component Organization
|
||||
|
||||
- **Route-specific**: Use `_components/` directories
|
||||
- **Route utilities**: Use `_lib/` for client, `_lib/server/` for server-side
|
||||
- **Global components**: Root-level directories
|
||||
|
||||
Example:
|
||||
|
||||
- Team components: `app/home/[account]/_components/`
|
||||
- Team server utils: `app/home/[account]/_lib/server/`
|
||||
- Marketing components: `app/(marketing)/_components/`
|
||||
|
||||
## Data Fetching Strategy
|
||||
|
||||
**Quick Decision Framework:**
|
||||
|
||||
- **Server Components**: Default choice for initial data loading
|
||||
- **Client Components**: For interactive features requiring hooks or real-time updates
|
||||
- **Admin Client**: Only for bypassing RLS (rare cases - requires manual auth/authorization)
|
||||
|
||||
### Server Components (Preferred) ✅
|
||||
|
||||
```typescript
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
async function NotesPage() {
|
||||
const client = getSupabaseServerClient();
|
||||
const { data, error } = await client.from('notes').select('*');
|
||||
|
||||
if (error) return <ErrorMessage error={error} />;
|
||||
return <NotesList notes={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Key Insight**: Server Components automatically inherit RLS protection - no additional authorization checks needed!
|
||||
|
||||
### Client Components (Interactive) 🖱️
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
function InteractiveNotes() {
|
||||
const supabase = useSupabase();
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['notes'],
|
||||
queryFn: () => supabase.from('notes').select('*')
|
||||
});
|
||||
|
||||
if (isLoading) return <Spinner />;
|
||||
return <NotesList notes={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Optimization - Parallel Data Fetching 🚀
|
||||
|
||||
**Sequential (Slow) Pattern ❌**
|
||||
|
||||
```typescript
|
||||
async function SlowDashboard() {
|
||||
const userData = await loadUserData();
|
||||
const notifications = await loadNotifications();
|
||||
const metrics = await loadMetrics();
|
||||
// Total time: sum of all requests
|
||||
}
|
||||
```
|
||||
|
||||
**Parallel (Optimized) Pattern ✅**
|
||||
|
||||
```typescript
|
||||
async function FastDashboard() {
|
||||
// Execute all requests simultaneously
|
||||
const [userData, notifications, metrics] = await Promise.all([
|
||||
loadUserData(),
|
||||
loadNotifications(),
|
||||
loadMetrics()
|
||||
]);
|
||||
// Total time: longest single request
|
||||
|
||||
return <Dashboard user={userData} notifications={notifications} metrics={metrics} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Impact**: Parallel fetching can reduce page load time by 60-80% for multi-data pages!
|
||||
|
||||
## Authorization Patterns - Critical Understanding 🔐
|
||||
|
||||
### RLS-Protected Data Fetching (Standard) ✅
|
||||
|
||||
```typescript
|
||||
async function getUserNotes(userId: string) {
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
// RLS automatically ensures user can only access their own notes
|
||||
// NO additional authorization checks needed!
|
||||
const { data } = await client.from('notes').select('*').eq('user_id', userId); // RLS validates this automatically
|
||||
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
### Admin Client Usage (Dangerous - Rare Cases Only) ⚠️
|
||||
|
||||
```typescript
|
||||
async function adminGetUserNotes(userId: string) {
|
||||
const adminClient = getSupabaseServerAdminClient();
|
||||
|
||||
// CRITICAL: Manual authorization required - bypasses RLS!
|
||||
const currentUser = await getCurrentUser();
|
||||
if (!(await isSuperAdmin(currentUser))) {
|
||||
throw new Error('Unauthorized: Admin access required');
|
||||
}
|
||||
|
||||
// Additional validation: ensure current admin isn't targeting themselves
|
||||
if (currentUser.id === userId) {
|
||||
throw new Error('Cannot perform admin action on own account');
|
||||
}
|
||||
|
||||
// Now safe to proceed with admin privileges
|
||||
const { data } = await adminClient
|
||||
.from('notes')
|
||||
.select('*')
|
||||
.eq('user_id', userId);
|
||||
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
**Rule of thumb**: If using standard Supabase client, trust RLS. If using admin client, validate everything manually.
|
||||
|
||||
## Internationalization
|
||||
|
||||
Always use `Trans` component from `@kit/ui/trans`:
|
||||
|
||||
```tsx
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
<Trans
|
||||
i18nKey="user:welcomeMessage"
|
||||
values={{ name: user.name }}
|
||||
/>
|
||||
|
||||
// With HTML elements
|
||||
<Trans
|
||||
i18nKey="terms:agreement"
|
||||
components={{
|
||||
TermsLink: <a href="/terms" className="underline" />,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Adding New Languages
|
||||
|
||||
1. Add language code to `lib/i18n/i18n.settings.ts`
|
||||
2. Create translation files in `public/locales/[new-language]/`
|
||||
3. Copy structure from English files
|
||||
|
||||
Translation files: `public/locales/<locale>/<namespace>.json`
|
||||
|
||||
## Workspace Contexts 🏢
|
||||
|
||||
### Personal Account Context (`app/home/(user)`)
|
||||
|
||||
```tsx
|
||||
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
|
||||
|
||||
function PersonalComponent() {
|
||||
const { user, account } = useUserWorkspace();
|
||||
// Personal account data
|
||||
}
|
||||
```
|
||||
|
||||
Context provider: `@packages/features/accounts/src/components/user-workspace-context-provider.tsx`
|
||||
|
||||
### Team Account Context (`app/home/[account]`)
|
||||
|
||||
```tsx
|
||||
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
|
||||
|
||||
function TeamComponent() {
|
||||
const { account, user, accounts } = useTeamAccountWorkspace();
|
||||
// Team account data with permissions
|
||||
}
|
||||
```
|
||||
|
||||
Context provider: `@packages/features/team-accounts/src/components/team-account-workspace-context-provider.tsx`
|
||||
|
||||
## Key Configuration Files
|
||||
|
||||
- **Feature flags**: `config/feature-flags.config.ts`
|
||||
- **i18n settings**: `lib/i18n/i18n.settings.ts`
|
||||
- **Supabase config**: `supabase/config.toml`
|
||||
- **Middleware**: `middleware.ts`
|
||||
|
||||
## Route Handlers (API Routes)
|
||||
|
||||
Use `enhanceRouteHandler` from `@packages/next/src/routes/index.ts`:
|
||||
|
||||
```typescript
|
||||
import { enhanceRouteHandler } from '@kit/next/routes';
|
||||
|
||||
export const POST = enhanceRouteHandler(
|
||||
async function ({ body, user, request }) {
|
||||
// body is validated, user available if auth: true
|
||||
return NextResponse.json({ success: true });
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
schema: ZodSchema,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Security Guidelines 🛡️
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
- Authentication already enforced by middleware
|
||||
- Authorization handled by RLS at database level (in most cases)
|
||||
- Avoid defensive code - use RLS instead
|
||||
- When using the Supabase admin client, must enforce both authentication and authorization
|
||||
|
||||
### Passing data to the client
|
||||
|
||||
- **Never pass sensitive data** to Client Components
|
||||
- **Never expose server environment variables** to client (unless prefixed with NEXT_PUBLIC)
|
||||
- Always validate user input
|
||||
|
||||
### Super Admin Protection
|
||||
|
||||
For admin routes, use `AdminGuard` from `@packages/features/admin/src/components/admin-guard.tsx`:
|
||||
|
||||
```tsx
|
||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||
|
||||
export default AdminGuard(AdminPageComponent);
|
||||
```
|
||||
264
apps/web/CLAUDE.md
Normal file
264
apps/web/CLAUDE.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# Web Application Instructions
|
||||
|
||||
This file contains instructions specific to the main Next.js web application.
|
||||
|
||||
## Application Structure
|
||||
|
||||
### Route Organization
|
||||
|
||||
```
|
||||
app/
|
||||
├── (marketing)/ # Public pages (landing, blog, docs)
|
||||
├── (auth)/ # Authentication pages
|
||||
├── home/
|
||||
│ ├── (user)/ # Personal account context
|
||||
│ └── [account]/ # Team account context ([account] = team slug)
|
||||
├── admin/ # Super admin section
|
||||
└── api/ # API routes
|
||||
```
|
||||
|
||||
Key Examples:
|
||||
|
||||
- Marketing layout: `app/(marketing)/layout.tsx`
|
||||
- Personal dashboard: `app/home/(user)/page.tsx`
|
||||
- Team workspace: `app/home/[account]/page.tsx`
|
||||
- Admin section: `app/admin/page.tsx`
|
||||
|
||||
### Component Organization
|
||||
|
||||
- **Route-specific**: Use `_components/` directories
|
||||
- **Route utilities**: Use `_lib/` for client, `_lib/server/` for server-side
|
||||
- **Global components**: Root-level directories
|
||||
|
||||
Example:
|
||||
|
||||
- Team components: `app/home/[account]/_components/`
|
||||
- Team server utils: `app/home/[account]/_lib/server/`
|
||||
- Marketing components: `app/(marketing)/_components/`
|
||||
|
||||
## Data Fetching Strategy
|
||||
|
||||
**Quick Decision Framework:**
|
||||
|
||||
- **Server Components**: Default choice for initial data loading
|
||||
- **Client Components**: For interactive features requiring hooks or real-time updates
|
||||
- **Admin Client**: Only for bypassing RLS (rare cases - requires manual auth/authorization)
|
||||
|
||||
### Server Components (Preferred) ✅
|
||||
|
||||
```typescript
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
async function NotesPage() {
|
||||
const client = getSupabaseServerClient();
|
||||
const { data, error } = await client.from('notes').select('*');
|
||||
|
||||
if (error) return <ErrorMessage error={error} />;
|
||||
return <NotesList notes={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Key Insight**: Server Components automatically inherit RLS protection - no additional authorization checks needed!
|
||||
|
||||
### Client Components (Interactive) 🖱️
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
function InteractiveNotes() {
|
||||
const supabase = useSupabase();
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['notes'],
|
||||
queryFn: () => supabase.from('notes').select('*')
|
||||
});
|
||||
|
||||
if (isLoading) return <Spinner />;
|
||||
return <NotesList notes={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Optimization - Parallel Data Fetching 🚀
|
||||
|
||||
**Sequential (Slow) Pattern ❌**
|
||||
|
||||
```typescript
|
||||
async function SlowDashboard() {
|
||||
const userData = await loadUserData();
|
||||
const notifications = await loadNotifications();
|
||||
const metrics = await loadMetrics();
|
||||
// Total time: sum of all requests
|
||||
}
|
||||
```
|
||||
|
||||
**Parallel (Optimized) Pattern ✅**
|
||||
|
||||
```typescript
|
||||
async function FastDashboard() {
|
||||
// Execute all requests simultaneously
|
||||
const [userData, notifications, metrics] = await Promise.all([
|
||||
loadUserData(),
|
||||
loadNotifications(),
|
||||
loadMetrics()
|
||||
]);
|
||||
// Total time: longest single request
|
||||
|
||||
return <Dashboard user={userData} notifications={notifications} metrics={metrics} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Impact**: Parallel fetching can reduce page load time by 60-80% for multi-data pages!
|
||||
|
||||
## Authorization Patterns - Critical Understanding 🔐
|
||||
|
||||
### RLS-Protected Data Fetching (Standard) ✅
|
||||
|
||||
```typescript
|
||||
async function getUserNotes(userId: string) {
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
// RLS automatically ensures user can only access their own notes
|
||||
// NO additional authorization checks needed!
|
||||
const { data } = await client.from('notes').select('*').eq('user_id', userId); // RLS validates this automatically
|
||||
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
### Admin Client Usage (Dangerous - Rare Cases Only) ⚠️
|
||||
|
||||
```typescript
|
||||
async function adminGetUserNotes(userId: string) {
|
||||
const adminClient = getSupabaseServerAdminClient();
|
||||
|
||||
// CRITICAL: Manual authorization required - bypasses RLS!
|
||||
const currentUser = await getCurrentUser();
|
||||
if (!(await isSuperAdmin(currentUser))) {
|
||||
throw new Error('Unauthorized: Admin access required');
|
||||
}
|
||||
|
||||
// Additional validation: ensure current admin isn't targeting themselves
|
||||
if (currentUser.id === userId) {
|
||||
throw new Error('Cannot perform admin action on own account');
|
||||
}
|
||||
|
||||
// Now safe to proceed with admin privileges
|
||||
const { data } = await adminClient
|
||||
.from('notes')
|
||||
.select('*')
|
||||
.eq('user_id', userId);
|
||||
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
**Rule of thumb**: If using standard Supabase client, trust RLS. If using admin client, validate everything manually.
|
||||
|
||||
## Internationalization
|
||||
|
||||
Always use `Trans` component from `@kit/ui/trans`:
|
||||
|
||||
```tsx
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
<Trans
|
||||
i18nKey="user:welcomeMessage"
|
||||
values={{ name: user.name }}
|
||||
/>
|
||||
|
||||
// With HTML elements
|
||||
<Trans
|
||||
i18nKey="terms:agreement"
|
||||
components={{
|
||||
TermsLink: <a href="/terms" className="underline" />,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Adding New Languages
|
||||
|
||||
1. Add language code to `lib/i18n/i18n.settings.ts`
|
||||
2. Create translation files in `public/locales/[new-language]/`
|
||||
3. Copy structure from English files
|
||||
|
||||
Translation files: `public/locales/<locale>/<namespace>.json`
|
||||
|
||||
## Workspace Contexts 🏢
|
||||
|
||||
### Personal Account Context (`app/home/(user)`)
|
||||
|
||||
```tsx
|
||||
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
|
||||
|
||||
function PersonalComponent() {
|
||||
const { user, account } = useUserWorkspace();
|
||||
// Personal account data
|
||||
}
|
||||
```
|
||||
|
||||
Context provider: `@packages/features/accounts/src/components/user-workspace-context-provider.tsx`
|
||||
|
||||
### Team Account Context (`app/home/[account]`)
|
||||
|
||||
```tsx
|
||||
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
|
||||
|
||||
function TeamComponent() {
|
||||
const { account, user, accounts } = useTeamAccountWorkspace();
|
||||
// Team account data with permissions
|
||||
}
|
||||
```
|
||||
|
||||
Context provider: `@packages/features/team-accounts/src/components/team-account-workspace-context-provider.tsx`
|
||||
|
||||
## Key Configuration Files
|
||||
|
||||
- **Feature flags**: `config/feature-flags.config.ts`
|
||||
- **i18n settings**: `lib/i18n/i18n.settings.ts`
|
||||
- **Supabase config**: `supabase/config.toml`
|
||||
- **Middleware**: `middleware.ts`
|
||||
|
||||
## Route Handlers (API Routes)
|
||||
|
||||
Use `enhanceRouteHandler` from `@packages/next/src/routes/index.ts`:
|
||||
|
||||
```typescript
|
||||
import { enhanceRouteHandler } from '@kit/next/routes';
|
||||
|
||||
export const POST = enhanceRouteHandler(
|
||||
async function ({ body, user, request }) {
|
||||
// body is validated, user available if auth: true
|
||||
return NextResponse.json({ success: true });
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
schema: ZodSchema,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Security Guidelines 🛡️
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
- Authentication already enforced by middleware
|
||||
- Authorization handled by RLS at database level (in most cases)
|
||||
- Avoid defensive code - use RLS instead
|
||||
- When using the Supabase admin client, must enforce both authentication and authorization
|
||||
|
||||
### Passing data to the client
|
||||
|
||||
- **Never pass sensitive data** to Client Components
|
||||
- **Never expose server environment variables** to client (unless prefixed with NEXT_PUBLIC)
|
||||
- Always validate user input
|
||||
|
||||
### Super Admin Protection
|
||||
|
||||
For admin routes, use `AdminGuard` from `@packages/features/admin/src/components/admin-guard.tsx`:
|
||||
|
||||
```tsx
|
||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||
|
||||
export default AdminGuard(AdminPageComponent);
|
||||
```
|
||||
3
apps/web/app/admin/accounts/loading.tsx
Normal file
3
apps/web/app/admin/accounts/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { GlobalLoader } from '@kit/ui/global-loader';
|
||||
|
||||
export default GlobalLoader;
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edge-csrf/nextjs": "2.5.3-cloudflare-rc1",
|
||||
"@hookform/resolvers": "^5.2.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@kit/accounts": "workspace:*",
|
||||
"@kit/admin": "workspace:*",
|
||||
"@kit/analytics": "workspace:*",
|
||||
@@ -53,15 +53,15 @@
|
||||
"@kit/ui": "workspace:*",
|
||||
"@makerkit/data-loader-supabase-core": "^0.0.10",
|
||||
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
|
||||
"@marsidev/react-turnstile": "^1.3.0",
|
||||
"@marsidev/react-turnstile": "^1.3.1",
|
||||
"@nosecone/next": "1.0.0-beta.11",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@supabase/supabase-js": "2.57.2",
|
||||
"@tanstack/react-query": "5.87.1",
|
||||
"@supabase/supabase-js": "2.57.4",
|
||||
"@tanstack/react-query": "5.89.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.542.0",
|
||||
"next": "15.5.2",
|
||||
"lucide-react": "^0.544.0",
|
||||
"next": "15.5.3",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"next-themes": "0.4.6",
|
||||
"react": "19.1.1",
|
||||
@@ -76,16 +76,16 @@
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@next/bundle-analyzer": "15.5.2",
|
||||
"@next/bundle-analyzer": "15.5.3",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/node": "^24.5.0",
|
||||
"@types/react": "19.1.13",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
"cssnano": "^7.1.1",
|
||||
"pino-pretty": "13.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"supabase": "2.39.2",
|
||||
"supabase": "2.40.7",
|
||||
"tailwindcss": "4.1.13",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.9.2"
|
||||
|
||||
292
apps/web/supabase/AGENTS.md
Normal file
292
apps/web/supabase/AGENTS.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# Supabase Database Schema Management
|
||||
|
||||
This file contains guidance for working with database schemas, migrations, and Supabase development workflows.
|
||||
|
||||
## Schema Organization
|
||||
|
||||
Schemas are organized in numbered files in the `schemas/` directory. Numbers are used to sort dependencies.
|
||||
|
||||
## Schema Development Workflow
|
||||
|
||||
### 1. Creating New Schema Files
|
||||
|
||||
```bash
|
||||
# Create new schema file
|
||||
touch schemas/15-my-new-feature.sql
|
||||
|
||||
# Apply changes and create migration
|
||||
pnpm --filter web run supabase:db:diff -f my-new-feature
|
||||
|
||||
# Restart Supabase with fresh schema
|
||||
pnpm supabase:web:reset
|
||||
|
||||
# Generate TypeScript types
|
||||
pnpm supabase:web:typegen
|
||||
```
|
||||
|
||||
### 2. Modifying Existing Schemas
|
||||
|
||||
```bash
|
||||
# Edit schema file (e.g., schemas/03-accounts.sql)
|
||||
# Make your changes...
|
||||
|
||||
# Create migration for changes
|
||||
pnpm --filter web run supabase:db:diff -f update-accounts
|
||||
|
||||
# Apply and test
|
||||
pnpm supabase:web:reset
|
||||
pnpm supabase:web:typegen
|
||||
```
|
||||
|
||||
## Security First Patterns
|
||||
|
||||
## Add permissions (if any)
|
||||
|
||||
```sql
|
||||
ALTER TYPE public.app_permissions ADD VALUE 'notes.manage';
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
### Table Creation with RLS
|
||||
|
||||
```sql
|
||||
-- Create table
|
||||
create table if not exists public.notes (
|
||||
id uuid unique not null default extensions.uuid_generate_v4(),
|
||||
account_id uuid references public.accounts(id) on delete cascade not null,
|
||||
-- ...
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
-- CRITICAL: Always enable RLS
|
||||
alter table "public"."notes" enable row level security;
|
||||
|
||||
-- Revoke default permissions
|
||||
revoke all on public.notes from authenticated, service_role;
|
||||
|
||||
-- Grant specific permissions
|
||||
grant select, insert, update, delete on table public.notes to authenticated;
|
||||
|
||||
-- Add RLS policies
|
||||
create policy "notes_read" on public.notes for select
|
||||
to authenticated using (
|
||||
account_id = (select auth.uid()) or
|
||||
public.has_role_on_account(account_id)
|
||||
);
|
||||
|
||||
create policy "notes_write" on public.notes for insert
|
||||
to authenticated with check (
|
||||
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
|
||||
);
|
||||
|
||||
create policy "notes_update" on public.notes for update
|
||||
to authenticated using (
|
||||
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
|
||||
)
|
||||
with check (
|
||||
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
|
||||
);
|
||||
|
||||
create policy "notes_delete" on public.notes for delete
|
||||
to authenticated using (
|
||||
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
|
||||
);
|
||||
```
|
||||
|
||||
### Storage Bucket Policies
|
||||
|
||||
```sql
|
||||
-- Create storage bucket
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('documents', 'documents', false);
|
||||
|
||||
-- RLS policy for storage
|
||||
create policy documents_policy on storage.objects for all using (
|
||||
bucket_id = 'documents'
|
||||
and (
|
||||
-- File belongs to user's account
|
||||
kit.get_storage_filename_as_uuid(name) = auth.uid()
|
||||
or
|
||||
-- User has access to the account
|
||||
public.has_role_on_account(kit.get_storage_filename_as_uuid(name))
|
||||
)
|
||||
)
|
||||
with check (
|
||||
bucket_id = 'documents'
|
||||
and (
|
||||
kit.get_storage_filename_as_uuid(name) = auth.uid()
|
||||
or
|
||||
public.has_permission(
|
||||
auth.uid(),
|
||||
kit.get_storage_filename_as_uuid(name),
|
||||
'files.upload'::app_permissions
|
||||
)
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## Function Creation Patterns
|
||||
|
||||
### Safe Security Definer Functions
|
||||
|
||||
```sql
|
||||
-- NEVER create security definer functions without explicit access controls
|
||||
create or replace function public.create_team_account(account_name text)
|
||||
returns public.accounts
|
||||
language plpgsql
|
||||
security definer -- Elevated privileges
|
||||
set search_path = '' -- Prevent SQL injection
|
||||
as $$
|
||||
declare
|
||||
new_account public.accounts;
|
||||
begin
|
||||
-- CRITICAL: Validate permissions first
|
||||
if not public.is_set('enable_team_accounts') then
|
||||
raise exception 'Team accounts are not enabled';
|
||||
end if;
|
||||
|
||||
-- Additional validation can go here
|
||||
if length(account_name) < 3 then
|
||||
raise exception 'Account name must be at least 3 characters';
|
||||
end if;
|
||||
|
||||
-- Now safe to proceed with elevated privileges
|
||||
insert into public.accounts (name, is_personal_account)
|
||||
values (account_name, false)
|
||||
returning * into new_account;
|
||||
|
||||
return new_account;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Grant to authenticated users only
|
||||
grant execute on function public.create_team_account(text) to authenticated;
|
||||
```
|
||||
|
||||
### Security Invoker Functions (Safer)
|
||||
|
||||
```sql
|
||||
-- Preferred: Functions that inherit RLS policies
|
||||
create or replace function public.get_account_notes(target_account_id uuid)
|
||||
returns setof public.notes
|
||||
language plpgsql
|
||||
security invoker -- Inherits caller's permissions (RLS applies)
|
||||
set search_path = ''
|
||||
as $$
|
||||
begin
|
||||
-- RLS policies will automatically restrict results
|
||||
return query
|
||||
select * from public.notes
|
||||
where account_id = target_account_id
|
||||
order by created_at desc;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function public.get_account_notes(uuid) to authenticated;
|
||||
```
|
||||
|
||||
### Safe Column Additions
|
||||
|
||||
```sql
|
||||
-- Safe: Add nullable columns
|
||||
alter table public.accounts
|
||||
add column if not exists description text;
|
||||
|
||||
-- Safe: Add columns with defaults
|
||||
alter table public.accounts
|
||||
add column if not exists is_verified boolean default false not null;
|
||||
|
||||
-- Unsafe: Adding non-null columns without defaults
|
||||
-- alter table public.accounts add column required_field text not null; -- DON'T DO THIS
|
||||
```
|
||||
|
||||
### Index Management
|
||||
|
||||
```sql
|
||||
-- Create indexes concurrently for large tables
|
||||
create index concurrently if not exists ix_accounts_created_at
|
||||
on public.accounts (created_at desc);
|
||||
|
||||
-- Drop unused indexes
|
||||
drop index if exists ix_old_unused_index;
|
||||
```
|
||||
|
||||
## Testing Database Changes
|
||||
|
||||
### Local Testing
|
||||
|
||||
```bash
|
||||
# Test with fresh database
|
||||
pnpm supabase:web:reset
|
||||
|
||||
# Test your changes
|
||||
pnpm run supabase:web:test
|
||||
```
|
||||
|
||||
## Type Generation
|
||||
|
||||
### After Schema Changes
|
||||
|
||||
```bash
|
||||
# Generate types after any schema changes
|
||||
pnpm supabase:web:typegen
|
||||
# Types are generated to src/lib/supabase/database.types.ts
|
||||
|
||||
# Reset DB
|
||||
pnpm supabase:web:reset
|
||||
```
|
||||
|
||||
### Using Generated Types
|
||||
|
||||
```typescript
|
||||
import { Enums, Tables } from '@kit/supabase/database';
|
||||
|
||||
// Table types
|
||||
type Account = Tables<'accounts'>;
|
||||
type Note = Tables<'notes'>;
|
||||
|
||||
// Enum types
|
||||
type AppPermission = Enums<'app_permissions'>;
|
||||
|
||||
// Insert types
|
||||
type AccountInsert = Tables<'accounts'>['Insert'];
|
||||
type AccountUpdate = Tables<'accounts'>['Update'];
|
||||
|
||||
// Use in functions
|
||||
async function createNote(data: Tables<'notes'>['Insert']) {
|
||||
const { data: note, error } = await supabase
|
||||
.from('notes')
|
||||
.insert(data)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
return note;
|
||||
}
|
||||
```
|
||||
|
||||
## Common Schema Patterns
|
||||
|
||||
### Audit Trail
|
||||
|
||||
Add triggers if the properties exist and are appropriate:
|
||||
|
||||
- `public.trigger_set_timestamps()` - for tables with `created_at` and `updated_at`
|
||||
columns
|
||||
- `public.trigger_set_user_tracking()` - for tables with `created_by` and `updated_by`
|
||||
columns
|
||||
|
||||
### Useful Commands
|
||||
|
||||
```bash
|
||||
# View migration status
|
||||
pnpm --filter web supabase migration list
|
||||
|
||||
# Reset database completely
|
||||
pnpm supabase:web:reset
|
||||
|
||||
# Generate migration from schema diff
|
||||
pnpm --filter web run supabase:db:diff -f migration-name
|
||||
|
||||
# Apply specific migration
|
||||
pnpm --filter web supabase migration up --include-schemas public
|
||||
```
|
||||
292
apps/web/supabase/CLAUDE.md
Normal file
292
apps/web/supabase/CLAUDE.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# Supabase Database Schema Management
|
||||
|
||||
This file contains guidance for working with database schemas, migrations, and Supabase development workflows.
|
||||
|
||||
## Schema Organization
|
||||
|
||||
Schemas are organized in numbered files in the `schemas/` directory. Numbers are used to sort dependencies.
|
||||
|
||||
## Schema Development Workflow
|
||||
|
||||
### 1. Creating New Schema Files
|
||||
|
||||
```bash
|
||||
# Create new schema file
|
||||
touch schemas/15-my-new-feature.sql
|
||||
|
||||
# Apply changes and create migration
|
||||
pnpm --filter web run supabase:db:diff -f my-new-feature
|
||||
|
||||
# Restart Supabase with fresh schema
|
||||
pnpm supabase:web:reset
|
||||
|
||||
# Generate TypeScript types
|
||||
pnpm supabase:web:typegen
|
||||
```
|
||||
|
||||
### 2. Modifying Existing Schemas
|
||||
|
||||
```bash
|
||||
# Edit schema file (e.g., schemas/03-accounts.sql)
|
||||
# Make your changes...
|
||||
|
||||
# Create migration for changes
|
||||
pnpm --filter web run supabase:db:diff -f update-accounts
|
||||
|
||||
# Apply and test
|
||||
pnpm supabase:web:reset
|
||||
pnpm supabase:web:typegen
|
||||
```
|
||||
|
||||
## Security First Patterns
|
||||
|
||||
## Add permissions (if any)
|
||||
|
||||
```sql
|
||||
ALTER TYPE public.app_permissions ADD VALUE 'notes.manage';
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
### Table Creation with RLS
|
||||
|
||||
```sql
|
||||
-- Create table
|
||||
create table if not exists public.notes (
|
||||
id uuid unique not null default extensions.uuid_generate_v4(),
|
||||
account_id uuid references public.accounts(id) on delete cascade not null,
|
||||
-- ...
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
-- CRITICAL: Always enable RLS
|
||||
alter table "public"."notes" enable row level security;
|
||||
|
||||
-- Revoke default permissions
|
||||
revoke all on public.notes from authenticated, service_role;
|
||||
|
||||
-- Grant specific permissions
|
||||
grant select, insert, update, delete on table public.notes to authenticated;
|
||||
|
||||
-- Add RLS policies
|
||||
create policy "notes_read" on public.notes for select
|
||||
to authenticated using (
|
||||
account_id = (select auth.uid()) or
|
||||
public.has_role_on_account(account_id)
|
||||
);
|
||||
|
||||
create policy "notes_write" on public.notes for insert
|
||||
to authenticated with check (
|
||||
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
|
||||
);
|
||||
|
||||
create policy "notes_update" on public.notes for update
|
||||
to authenticated using (
|
||||
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
|
||||
)
|
||||
with check (
|
||||
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
|
||||
);
|
||||
|
||||
create policy "notes_delete" on public.notes for delete
|
||||
to authenticated using (
|
||||
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
|
||||
);
|
||||
```
|
||||
|
||||
### Storage Bucket Policies
|
||||
|
||||
```sql
|
||||
-- Create storage bucket
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('documents', 'documents', false);
|
||||
|
||||
-- RLS policy for storage
|
||||
create policy documents_policy on storage.objects for all using (
|
||||
bucket_id = 'documents'
|
||||
and (
|
||||
-- File belongs to user's account
|
||||
kit.get_storage_filename_as_uuid(name) = auth.uid()
|
||||
or
|
||||
-- User has access to the account
|
||||
public.has_role_on_account(kit.get_storage_filename_as_uuid(name))
|
||||
)
|
||||
)
|
||||
with check (
|
||||
bucket_id = 'documents'
|
||||
and (
|
||||
kit.get_storage_filename_as_uuid(name) = auth.uid()
|
||||
or
|
||||
public.has_permission(
|
||||
auth.uid(),
|
||||
kit.get_storage_filename_as_uuid(name),
|
||||
'files.upload'::app_permissions
|
||||
)
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## Function Creation Patterns
|
||||
|
||||
### Safe Security Definer Functions
|
||||
|
||||
```sql
|
||||
-- NEVER create security definer functions without explicit access controls
|
||||
create or replace function public.create_team_account(account_name text)
|
||||
returns public.accounts
|
||||
language plpgsql
|
||||
security definer -- Elevated privileges
|
||||
set search_path = '' -- Prevent SQL injection
|
||||
as $$
|
||||
declare
|
||||
new_account public.accounts;
|
||||
begin
|
||||
-- CRITICAL: Validate permissions first
|
||||
if not public.is_set('enable_team_accounts') then
|
||||
raise exception 'Team accounts are not enabled';
|
||||
end if;
|
||||
|
||||
-- Additional validation can go here
|
||||
if length(account_name) < 3 then
|
||||
raise exception 'Account name must be at least 3 characters';
|
||||
end if;
|
||||
|
||||
-- Now safe to proceed with elevated privileges
|
||||
insert into public.accounts (name, is_personal_account)
|
||||
values (account_name, false)
|
||||
returning * into new_account;
|
||||
|
||||
return new_account;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Grant to authenticated users only
|
||||
grant execute on function public.create_team_account(text) to authenticated;
|
||||
```
|
||||
|
||||
### Security Invoker Functions (Safer)
|
||||
|
||||
```sql
|
||||
-- Preferred: Functions that inherit RLS policies
|
||||
create or replace function public.get_account_notes(target_account_id uuid)
|
||||
returns setof public.notes
|
||||
language plpgsql
|
||||
security invoker -- Inherits caller's permissions (RLS applies)
|
||||
set search_path = ''
|
||||
as $$
|
||||
begin
|
||||
-- RLS policies will automatically restrict results
|
||||
return query
|
||||
select * from public.notes
|
||||
where account_id = target_account_id
|
||||
order by created_at desc;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function public.get_account_notes(uuid) to authenticated;
|
||||
```
|
||||
|
||||
### Safe Column Additions
|
||||
|
||||
```sql
|
||||
-- Safe: Add nullable columns
|
||||
alter table public.accounts
|
||||
add column if not exists description text;
|
||||
|
||||
-- Safe: Add columns with defaults
|
||||
alter table public.accounts
|
||||
add column if not exists is_verified boolean default false not null;
|
||||
|
||||
-- Unsafe: Adding non-null columns without defaults
|
||||
-- alter table public.accounts add column required_field text not null; -- DON'T DO THIS
|
||||
```
|
||||
|
||||
### Index Management
|
||||
|
||||
```sql
|
||||
-- Create indexes concurrently for large tables
|
||||
create index concurrently if not exists ix_accounts_created_at
|
||||
on public.accounts (created_at desc);
|
||||
|
||||
-- Drop unused indexes
|
||||
drop index if exists ix_old_unused_index;
|
||||
```
|
||||
|
||||
## Testing Database Changes
|
||||
|
||||
### Local Testing
|
||||
|
||||
```bash
|
||||
# Test with fresh database
|
||||
pnpm supabase:web:reset
|
||||
|
||||
# Test your changes
|
||||
pnpm run supabase:web:test
|
||||
```
|
||||
|
||||
## Type Generation
|
||||
|
||||
### After Schema Changes
|
||||
|
||||
```bash
|
||||
# Generate types after any schema changes
|
||||
pnpm supabase:web:typegen
|
||||
# Types are generated to src/lib/supabase/database.types.ts
|
||||
|
||||
# Reset DB
|
||||
pnpm supabase:web:reset
|
||||
```
|
||||
|
||||
### Using Generated Types
|
||||
|
||||
```typescript
|
||||
import { Enums, Tables } from '@kit/supabase/database';
|
||||
|
||||
// Table types
|
||||
type Account = Tables<'accounts'>;
|
||||
type Note = Tables<'notes'>;
|
||||
|
||||
// Enum types
|
||||
type AppPermission = Enums<'app_permissions'>;
|
||||
|
||||
// Insert types
|
||||
type AccountInsert = Tables<'accounts'>['Insert'];
|
||||
type AccountUpdate = Tables<'accounts'>['Update'];
|
||||
|
||||
// Use in functions
|
||||
async function createNote(data: Tables<'notes'>['Insert']) {
|
||||
const { data: note, error } = await supabase
|
||||
.from('notes')
|
||||
.insert(data)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
return note;
|
||||
}
|
||||
```
|
||||
|
||||
## Common Schema Patterns
|
||||
|
||||
### Audit Trail
|
||||
|
||||
Add triggers if the properties exist and are appropriate:
|
||||
|
||||
- `public.trigger_set_timestamps()` - for tables with `created_at` and `updated_at`
|
||||
columns
|
||||
- `public.trigger_set_user_tracking()` - for tables with `created_by` and `updated_by`
|
||||
columns
|
||||
|
||||
### Useful Commands
|
||||
|
||||
```bash
|
||||
# View migration status
|
||||
pnpm --filter web supabase migration list
|
||||
|
||||
# Reset database completely
|
||||
pnpm supabase:web:reset
|
||||
|
||||
# Generate migration from schema diff
|
||||
pnpm --filter web run supabase:db:diff -f migration-name
|
||||
|
||||
# Apply specific migration
|
||||
pnpm --filter web supabase migration up --include-schemas public
|
||||
```
|
||||
42
apps/web/supabase/migrations/20250917024249_triggers.sql
Normal file
42
apps/web/supabase/migrations/20250917024249_triggers.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
-- Triggers for accounts table
|
||||
create trigger accounts_set_timestamps
|
||||
before insert or update on public.accounts
|
||||
for each row execute function public.trigger_set_timestamps();
|
||||
|
||||
create trigger accounts_set_user_tracking
|
||||
before insert or update on public.accounts
|
||||
for each row execute function public.trigger_set_user_tracking();
|
||||
|
||||
-- Triggers for accounts_memberships table
|
||||
create trigger accounts_memberships_set_timestamps
|
||||
before insert or update on public.accounts_memberships
|
||||
for each row execute function public.trigger_set_timestamps();
|
||||
|
||||
create trigger accounts_memberships_set_user_tracking
|
||||
before insert or update on public.accounts_memberships
|
||||
for each row execute function public.trigger_set_user_tracking();
|
||||
|
||||
-- Triggers for invitations table
|
||||
create trigger invitations_set_timestamps
|
||||
before insert or update on public.invitations
|
||||
for each row execute function public.trigger_set_timestamps();
|
||||
|
||||
-- Triggers for subscriptions table
|
||||
create trigger subscriptions_set_timestamps
|
||||
before insert or update on public.subscriptions
|
||||
for each row execute function public.trigger_set_timestamps();
|
||||
|
||||
-- Triggers for subscription_items table
|
||||
create trigger subscription_items_set_timestamps
|
||||
before insert or update on public.subscription_items
|
||||
for each row execute function public.trigger_set_timestamps();
|
||||
|
||||
-- Triggers for orders table
|
||||
create trigger orders_set_timestamps
|
||||
before insert or update on public.orders
|
||||
for each row execute function public.trigger_set_timestamps();
|
||||
|
||||
-- Triggers for order_items table
|
||||
create trigger order_items_set_timestamps
|
||||
before insert or update on public.order_items
|
||||
for each row execute function public.trigger_set_timestamps();
|
||||
@@ -77,6 +77,15 @@ create unique index unique_personal_account on public.accounts (primary_owner_us
|
||||
where
|
||||
is_personal_account = true;
|
||||
|
||||
-- Triggers for accounts table
|
||||
create trigger accounts_set_timestamps
|
||||
before insert or update on public.accounts
|
||||
for each row execute function public.trigger_set_timestamps();
|
||||
|
||||
create trigger accounts_set_user_tracking
|
||||
before insert or update on public.accounts
|
||||
for each row execute function public.trigger_set_user_tracking();
|
||||
|
||||
-- RLS on the accounts table
|
||||
-- UPDATE(accounts):
|
||||
-- Team owners can update their accounts
|
||||
|
||||
@@ -46,6 +46,15 @@ create index ix_accounts_memberships_user_id on public.accounts_memberships (use
|
||||
|
||||
create index ix_accounts_memberships_account_role on public.accounts_memberships (account_role);
|
||||
|
||||
-- Triggers for accounts_memberships table
|
||||
create trigger accounts_memberships_set_timestamps
|
||||
before insert or update on public.accounts_memberships
|
||||
for each row execute function public.trigger_set_timestamps();
|
||||
|
||||
create trigger accounts_memberships_set_user_tracking
|
||||
before insert or update on public.accounts_memberships
|
||||
for each row execute function public.trigger_set_user_tracking();
|
||||
|
||||
-- Enable RLS on the accounts_memberships table
|
||||
alter table public.accounts_memberships enable row level security;
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ comment on column public.invitations.email is 'The email of the user being invit
|
||||
-- Indexes on the invitations table
|
||||
create index ix_invitations_account_id on public.invitations (account_id);
|
||||
|
||||
-- Triggers for invitations table
|
||||
create trigger invitations_set_timestamps
|
||||
before insert or update on public.invitations
|
||||
for each row execute function public.trigger_set_timestamps();
|
||||
|
||||
-- Revoke all on invitations table from authenticated and service_role
|
||||
revoke all on public.invitations
|
||||
from
|
||||
|
||||
@@ -69,6 +69,11 @@ select
|
||||
-- Indexes on the subscriptions table
|
||||
create index ix_subscriptions_account_id on public.subscriptions (account_id);
|
||||
|
||||
-- Triggers for subscriptions table
|
||||
create trigger subscriptions_set_timestamps
|
||||
before insert or update on public.subscriptions
|
||||
for each row execute function public.trigger_set_timestamps();
|
||||
|
||||
-- Enable RLS on subscriptions table
|
||||
alter table public.subscriptions enable row level security;
|
||||
|
||||
@@ -314,6 +319,11 @@ delete on table public.subscription_items to service_role;
|
||||
-- Indexes on the subscription_items table
|
||||
create index ix_subscription_items_subscription_id on public.subscription_items (subscription_id);
|
||||
|
||||
-- Triggers for subscription_items table
|
||||
create trigger subscription_items_set_timestamps
|
||||
before insert or update on public.subscription_items
|
||||
for each row execute function public.trigger_set_timestamps();
|
||||
|
||||
-- RLS
|
||||
alter table public.subscription_items enable row level security;
|
||||
|
||||
|
||||
@@ -55,6 +55,11 @@ delete on table public.orders to service_role;
|
||||
-- Indexes on the orders table
|
||||
create index ix_orders_account_id on public.orders (account_id);
|
||||
|
||||
-- Triggers for orders table
|
||||
create trigger orders_set_timestamps
|
||||
before insert or update on public.orders
|
||||
for each row execute function public.trigger_set_timestamps();
|
||||
|
||||
-- RLS
|
||||
alter table public.orders enable row level security;
|
||||
|
||||
@@ -130,6 +135,11 @@ grant insert, update, delete on table public.order_items to service_role;
|
||||
-- Indexes on the order_items table
|
||||
create index ix_order_items_order_id on public.order_items (order_id);
|
||||
|
||||
-- Triggers for order_items table
|
||||
create trigger order_items_set_timestamps
|
||||
before insert or update on public.order_items
|
||||
for each row execute function public.trigger_set_timestamps();
|
||||
|
||||
-- RLS
|
||||
alter table public.order_items enable row level security;
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
BEGIN;
|
||||
create extension "basejump-supabase_test_helpers" version '0.0.6';
|
||||
|
||||
select plan(12);
|
||||
|
||||
--- Test the trigger_set_timestamps function on all tables
|
||||
--- This test verifies that created_at and updated_at are properly set on insert
|
||||
|
||||
--- Create test users
|
||||
select tests.create_supabase_user('trigger_test_user1', 'test1@example.com');
|
||||
|
||||
-- Authenticate as test user
|
||||
select makerkit.authenticate_as('trigger_test_user1');
|
||||
|
||||
------------
|
||||
--- Test accounts table timestamp triggers - INSERT
|
||||
------------
|
||||
|
||||
INSERT INTO public.accounts (name, is_personal_account)
|
||||
VALUES ('Test Account', false);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT created_at IS NOT NULL FROM public.accounts WHERE name = 'Test Account'),
|
||||
'accounts: created_at should be set automatically on insert'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT updated_at IS NOT NULL FROM public.accounts WHERE name = 'Test Account'),
|
||||
'accounts: updated_at should be set automatically on insert'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT created_at = updated_at FROM public.accounts WHERE name = 'Test Account'),
|
||||
'accounts: created_at should equal updated_at on insert'
|
||||
);
|
||||
|
||||
------------
|
||||
--- Test invitations table timestamp triggers - INSERT
|
||||
------------
|
||||
|
||||
-- Create a team account for invitation testing
|
||||
INSERT INTO public.accounts (name, is_personal_account)
|
||||
VALUES ('Invitation Test Team', false);
|
||||
|
||||
-- Test invitation insert
|
||||
INSERT INTO public.invitations (email, account_id, invited_by, role, invite_token, expires_at)
|
||||
VALUES (
|
||||
'invitee@example.com',
|
||||
(SELECT id FROM public.accounts WHERE name = 'Invitation Test Team'),
|
||||
tests.get_supabase_uid('trigger_test_user1'),
|
||||
'member',
|
||||
'test-token-123',
|
||||
now() + interval '7 days'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT created_at IS NOT NULL FROM public.invitations WHERE email = 'invitee@example.com'),
|
||||
'invitations: created_at should be set automatically on insert'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT updated_at IS NOT NULL FROM public.invitations WHERE email = 'invitee@example.com'),
|
||||
'invitations: updated_at should be set automatically on insert'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT created_at = updated_at FROM public.invitations WHERE email = 'invitee@example.com'),
|
||||
'invitations: created_at should equal updated_at on insert'
|
||||
);
|
||||
|
||||
------------
|
||||
--- Test subscriptions table timestamp triggers - INSERT (service_role required)
|
||||
------------
|
||||
|
||||
set role service_role;
|
||||
|
||||
-- Create billing customer first
|
||||
INSERT INTO public.billing_customers (account_id, provider, customer_id, email)
|
||||
VALUES (
|
||||
(SELECT id FROM public.accounts WHERE name = 'Invitation Test Team'),
|
||||
'stripe',
|
||||
'cus_test123',
|
||||
'billing@example.com'
|
||||
);
|
||||
|
||||
-- Test subscription insert
|
||||
INSERT INTO public.subscriptions (
|
||||
id, account_id, billing_customer_id, status, active, billing_provider,
|
||||
cancel_at_period_end, currency, period_starts_at, period_ends_at
|
||||
)
|
||||
VALUES (
|
||||
'sub_test123',
|
||||
(SELECT id FROM public.accounts WHERE name = 'Invitation Test Team'),
|
||||
(SELECT id FROM public.billing_customers WHERE customer_id = 'cus_test123'),
|
||||
'active',
|
||||
true,
|
||||
'stripe',
|
||||
false,
|
||||
'USD',
|
||||
now(),
|
||||
now() + interval '1 month'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT created_at IS NOT NULL FROM public.subscriptions WHERE id = 'sub_test123'),
|
||||
'subscriptions: created_at should be set automatically on insert'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT updated_at IS NOT NULL FROM public.subscriptions WHERE id = 'sub_test123'),
|
||||
'subscriptions: updated_at should be set automatically on insert'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT created_at = updated_at FROM public.subscriptions WHERE id = 'sub_test123'),
|
||||
'subscriptions: created_at should equal updated_at on insert'
|
||||
);
|
||||
|
||||
------------
|
||||
--- Test subscription_items table timestamp triggers - INSERT
|
||||
------------
|
||||
|
||||
-- Test subscription_item insert
|
||||
INSERT INTO public.subscription_items (
|
||||
id, subscription_id, product_id, variant_id, type, quantity, interval, interval_count
|
||||
)
|
||||
VALUES (
|
||||
'si_test123',
|
||||
'sub_test123',
|
||||
'prod_test123',
|
||||
'var_test123',
|
||||
'flat',
|
||||
1,
|
||||
'month',
|
||||
1
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT created_at IS NOT NULL FROM public.subscription_items WHERE id = 'si_test123'),
|
||||
'subscription_items: created_at should be set automatically on insert'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT updated_at IS NOT NULL FROM public.subscription_items WHERE id = 'si_test123'),
|
||||
'subscription_items: updated_at should be set automatically on insert'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT created_at = updated_at FROM public.subscription_items WHERE id = 'si_test123'),
|
||||
'subscription_items: created_at should equal updated_at on insert'
|
||||
);
|
||||
|
||||
SELECT * FROM finish();
|
||||
|
||||
ROLLBACK;
|
||||
@@ -0,0 +1,43 @@
|
||||
BEGIN;
|
||||
create extension "basejump-supabase_test_helpers" version '0.0.6';
|
||||
|
||||
select plan(3);
|
||||
|
||||
--- Test the trigger_set_user_tracking function on accounts table
|
||||
--- This test verifies that created_by and updated_by are properly set on insert
|
||||
|
||||
--- Create test users
|
||||
select tests.create_supabase_user('user_tracking_test1', 'tracking1@example.com');
|
||||
|
||||
------------
|
||||
--- Test accounts table user tracking triggers - INSERT
|
||||
------------
|
||||
|
||||
-- Authenticate as first user for insert
|
||||
select makerkit.authenticate_as('user_tracking_test1');
|
||||
|
||||
-- Test INSERT: created_by and updated_by should be set to current user
|
||||
INSERT INTO public.accounts (name, is_personal_account)
|
||||
VALUES ('User Tracking Test Account', false);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT created_by = tests.get_supabase_uid('user_tracking_test1')
|
||||
FROM public.accounts WHERE name = 'User Tracking Test Account'),
|
||||
'accounts: created_by should be set to current user on insert'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT updated_by = tests.get_supabase_uid('user_tracking_test1')
|
||||
FROM public.accounts WHERE name = 'User Tracking Test Account'),
|
||||
'accounts: updated_by should be set to current user on insert'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT created_by = updated_by
|
||||
FROM public.accounts WHERE name = 'User Tracking Test Account'),
|
||||
'accounts: created_by should equal updated_by on insert'
|
||||
);
|
||||
|
||||
SELECT * FROM finish();
|
||||
|
||||
ROLLBACK;
|
||||
Reference in New Issue
Block a user