skills/ballee/flutter-query-testing/SKILL.md
Comprehensive Flutter Supabase query testing: static validation, live testing, RLS verification, and CI integration. Use when validating queries before deployment, debugging query failures, or setting up query testing pipelines.
npx skillsauth add javeedishaq/ai-workflow-orchestrator skills/ballee/flutter-query-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Comprehensive guide for validating Flutter Supabase queries against database schema, testing them locally, and integrating into CI/CD pipelines. Catch query errors before they reach production.
DO use this skill when:
DO NOT use this skill when:
db-lint-manager)# Static analysis (no DB needed)
cd apps/mobile
dart run scripts/validate_queries.dart
# Live testing (requires local Supabase)
dart run scripts/test_queries_local.dart
# Generate full report
dart run scripts/generate_query_report.dart
# Already configured in lefthook.yml
pre-commit:
flutter-query-static:
glob: "apps/mobile/lib/**/api/**/*.dart"
run: cd apps/mobile && dart run scripts/validate_queries.dart --changed-only
Ballee uses generated Supabase types in lib/core/generated/supabase/:
// Table names
CastAssignment.table_name // 'cast_assignments'
EventParticipant.table_name // 'event_participants'
// Column names (prefixed with c_)
CastAssignment.c_userId // 'user_id'
CastAssignment.c_assignmentStatus // 'assignment_status'
Profile.c_firstName // 'first_name'
// Simple query with generated constants
await _client
.from(CastAssignment.table_name)
.select()
.eq(CastAssignment.c_userId, userId);
// Nested relationships
await _client
.from(Conversations.table_name)
.select('''
*,
participants:${ConversationParticipants.table_name}!inner(
*,
profile:${Profile.table_name}!conversation_participants_user_id_profiles_fkey(
${Profile.c_id},
${Profile.c_firstName}
)
)
''');
// RPC calls
await _client.rpc(
'get_or_create_direct_conversation',
params: {'p_other_user_id': otherUserId},
);
Static analysis validates queries without running them.
| Check | Description | Example Error |
|-------|-------------|---------------|
| Table existence | Table name exists in schema | Table 'event_participations' not found |
| Column existence | Column exists on target table | Column 'name' not found on 'events' |
| Relationship validity | FK relationship exists | Unknown relationship 'venue' on 'events' |
| RPC function existence | Function exists in DB | RPC 'get_user_data' not found |
| Type constant usage | Generated constants are used | Warning: Use Profile.c_id instead of 'id' |
1. Parse all *_api.dart files
2. Extract Supabase calls:
- .from('table') → table name
- .select('columns') → column names
- .eq('column', value) → filter column
- .rpc('function') → RPC function
3. Load schema from lib/core/generated/supabase/
4. Validate each extracted element
5. Report errors with file:line references
Flutter Query Static Analysis
=============================
apps/mobile/lib/modules/schedule/api/schedule_api.dart
Line 28: ✓ .from(CastAssignment.table_name) → cast_assignments
Line 30: ✓ .select('id, assignment_status, ...')
Line 43: ✓ .eq(CastAssignment.c_userId, _userId)
apps/mobile/lib/modules/inbox/api/inbox_api.dart
Line 34: ✓ .from(Conversations.table_name) → conversations
Line 47: ✗ .eq('conversation_participants.user_id', ...)
Warning: Consider using generated constant
Summary: 45 queries validated, 0 errors, 2 warnings
Live testing executes queries against local Supabase to catch runtime errors.
# Start local Supabase
cd apps/web
pnpm supabase:web:start
# Verify it's running
curl http://localhost:54321/rest/v1/
| Test | Description | |------|-------------| | Query execution | Query runs without PostgreSQL errors | | Response structure | Response matches expected shape | | Performance | Query completes within threshold (default: 1000ms) | | RLS behavior | Query respects RLS policies |
// 1. Basic CRUD tests
// Verify queries execute successfully
// 2. Relationship tests
// Verify nested selects return correct structure
// 3. RLS tests
// Verify authenticated vs anonymous access
// 4. Edge case tests
// Empty results, null handling, special characters
Flutter Query Live Testing
==========================
Testing schedule_api.dart...
✓ getScheduleItems() - 3 rows in 45ms
✓ getUpcomingItems() - 1 row in 32ms
Testing inbox_api.dart...
✓ getConversations() - 2 rows in 78ms
✓ getConversation(id) - 1 row in 25ms
✗ getOrCreateDirectConversation() - RLS denied
Error: new row violates row-level security policy
Summary: 12 tests, 11 passed, 1 failed
Testing Row Level Security is critical for mobile apps.
// Test 1: Authenticated user access
test('authenticated user can read own data', () async {
await signInAsTestUser();
final result = await _client
.from('profiles')
.select()
.eq('id', testUserId);
expect(result, isNotEmpty);
});
// Test 2: Cross-user isolation
test('user cannot read other user data', () async {
await signInAsTestUser();
final result = await _client
.from('profiles')
.select()
.eq('id', otherUserId);
expect(result, isEmpty); // RLS blocks access
});
// Test 3: Anonymous access
test('anonymous cannot access protected tables', () async {
await signOut();
expect(
() => _client.from('profiles').select(),
throwsA(isA<PostgrestException>()),
);
});
// Test 4: Write permissions
test('user can only update own profile', () async {
await signInAsTestUser();
// Should succeed
await _client
.from('profiles')
.update({'bio': 'updated'})
.eq('id', testUserId);
// Should fail (RLS)
expect(
() => _client
.from('profiles')
.update({'bio': 'hacked'})
.eq('id', otherUserId),
throwsA(isA<PostgrestException>()),
);
});
| Table | Anonymous | Authenticated (own) | Authenticated (other) | Super Admin | |-------|-----------|---------------------|----------------------|-------------| | profiles | - | CRUD | R (limited) | CRUD | | events | R (public) | CRUD | R (public) | CRUD | | messages | - | CRUD (participant) | - | CRUD |
Full integration tests for API classes.
// test/modules/schedule/schedule_api_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
void main() {
late SupabaseClient client;
late ScheduleApi api;
setUpAll(() async {
// Initialize Supabase with local instance
await Supabase.initialize(
url: 'http://localhost:54321',
anonKey: 'eyJ...', // Local anon key
);
client = Supabase.instance.client;
api = ScheduleApi(client);
});
setUp(() async {
// Sign in as test user before each test
await client.auth.signInWithPassword(
email: '[email protected]',
password: 'test123',
);
});
tearDown(() async {
await client.auth.signOut();
});
group('ScheduleApi', () {
test('getScheduleItems returns items within date range', () async {
final items = await api.getScheduleItems(
startDate: DateTime(2026, 1, 1),
endDate: DateTime(2026, 1, 31),
);
expect(items, isA<List<ScheduleItemEntity>>());
for (final item in items) {
expect(item.startDateTime.isAfter(DateTime(2026, 1, 1)), isTrue);
expect(item.startDateTime.isBefore(DateTime(2026, 1, 31)), isTrue);
}
});
test('getUpcomingItems returns next 30 days', () async {
final items = await api.getUpcomingItems();
final now = DateTime.now();
final thirtyDaysLater = now.add(Duration(days: 30));
for (final item in items) {
expect(item.startDateTime.isAfter(now), isTrue);
expect(item.startDateTime.isBefore(thirtyDaysLater), isTrue);
}
});
});
}
// For unit tests without DB, mock the Supabase client
import 'package:mocktail/mocktail.dart';
class MockSupabaseClient extends Mock implements SupabaseClient {}
class MockSupabaseQueryBuilder extends Mock implements SupabaseQueryBuilder {}
void main() {
late MockSupabaseClient mockClient;
late ScheduleApi api;
setUp(() {
mockClient = MockSupabaseClient();
api = ScheduleApi(mockClient);
// Setup mock responses
final mockQueryBuilder = MockSupabaseQueryBuilder();
when(() => mockClient.from(any())).thenReturn(mockQueryBuilder);
when(() => mockQueryBuilder.select(any())).thenReturn(mockQueryBuilder);
when(() => mockQueryBuilder.eq(any(), any())).thenReturn(mockQueryBuilder);
when(() => mockQueryBuilder.execute()).thenAnswer(
(_) async => PostgrestResponse(data: [], count: 0),
);
});
test('calls correct table', () async {
await api.getUpcomingItems();
verify(() => mockClient.from('cast_assignments')).called(1);
});
}
# .github/workflows/flutter-quality.yml
name: Flutter Quality
on:
push:
paths:
- 'apps/mobile/**'
pull_request:
paths:
- 'apps/mobile/**'
jobs:
query-validation:
runs-on: ubuntu-latest
services:
postgres:
image: supabase/postgres:15.1.0.147
env:
POSTGRES_PASSWORD: postgres
ports:
- 54322:5432
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
channel: 'stable'
- name: Install dependencies
run: cd apps/mobile && flutter pub get
- name: Static Query Validation
run: cd apps/mobile && dart run scripts/validate_queries.dart
- name: Setup Supabase
run: |
cd apps/web
npx supabase start
- name: Live Query Testing
run: cd apps/mobile && dart run scripts/test_queries_local.dart
- name: Generate Report
run: cd apps/mobile && dart run scripts/generate_query_report.dart
- name: Upload Report
uses: actions/upload-artifact@v4
with:
name: query-report
path: apps/mobile/query-report.md
# lefthook.yml (additions)
pre-commit:
commands:
flutter-query-static:
glob: "apps/mobile/lib/**/api/**/*.dart"
run: |
cd apps/mobile && dart run scripts/validate_queries.dart --changed-only
fail_text: "Flutter query static validation failed"
pre-push:
commands:
flutter-query-live:
glob: "apps/mobile/**/*.dart"
run: |
if pgrep -f "supabase" > /dev/null; then
cd apps/mobile && dart run scripts/test_queries_local.dart
else
echo "Skipping live query tests (Supabase not running)"
fi
fail_text: "Flutter query live testing failed"
#!/bin/bash
# apps/mobile/scripts/sync_schema.sh
set -e
echo "Regenerating Supabase types..."
cd ../web
npx supabase gen types dart --local > ../mobile/lib/core/generated/supabase/schema.dart
echo "Validating queries against new schema..."
cd ../mobile
dart run scripts/validate_queries.dart
echo "Schema sync complete!"
Error: Column 'name' not found on table 'events'
Suggestion: Did you mean 'title'?
Fix: Use the correct column name. Check generated types in lib/core/generated/supabase/.
Error: Unknown relationship 'venue' on 'events'
Fix: Use explicit FK syntax:
// Before
'venue(name, city)'
// After
'venue:venues!events_venue_id_fkey(name, city)'
Error: RPC function 'get_user_data' not found
Fix: Verify function exists in migrations or create it:
CREATE OR REPLACE FUNCTION get_user_data(p_user_id uuid)
RETURNS json AS $$
-- function body
$$ LANGUAGE sql SECURITY DEFINER;
Warning: Using hardcoded 'user_id' instead of Profile.c_userId
Fix: Regenerate types:
cd apps/web && pnpm supabase:web:typegen
Error: new row violates row-level security policy
Fix: Check RLS policies in apps/web/supabase/migrations/. Common causes:
is_super_admin() bypassUSING clauseWITH CHECK for INSERT/UPDATEapps/mobile/lib/modules/*/api/*_api.dart
apps/mobile/lib/core/data/api/*_api.dart
apps/mobile/lib/core/generated/supabase/*.dart
apps/mobile/scripts/validate_queries.dart
apps/mobile/scripts/test_queries_local.dart
apps/mobile/scripts/generate_query_report.dart
apps/mobile/scripts/sync_schema.sh
apps/mobile/test/modules/*/api/*_api_test.dart
tools
# Test Patterns Testing patterns for reliable, maintainable, and fast tests. > **Template Usage:** Customize for your test framework (Vitest, Jest, Playwright, etc.) and assertion library. ## Test Structure ```typescript // user.test.ts import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { userService } from '@/services/user.service'; import { createTestUser, cleanupTestData } from '@/tests/helpers'; describe('UserService', () => { let testUserId: string; befor
tools
# State Management Patterns Client-side state management patterns for modern applications. > **Template Usage:** Customize for your state library (React Query, Zustand, Jotai, Redux, etc.). ## State Categories | Type | Description | Solution | |------|-------------|----------| | **Server State** | Data from API/database | React Query, SWR | | **Client State** | UI state, user preferences | Zustand, Jotai, useState | | **Form State** | Form inputs, validation | React Hook Form, Formik | | **U
development
# Service Patterns Service layer patterns for clean architecture with proper error handling, logging, and type safety. > **Template Usage:** Customize for your ORM (Prisma, Drizzle, TypeORM, etc.) and logging solution. ## Result Type Pattern Never throw exceptions from services. Always return a Result type. ```typescript // lib/result.ts export type Result<T, E = Error> = | { success: true; data: T } | { success: false; error: E }; export function ok<T>(data: T): Result<T, never> { r
testing
# Row-Level Security Patterns Database security patterns for multi-tenant and user-scoped data. > **Template Usage:** Customize for your database (PostgreSQL, Supabase, etc.) and auth system. ## RLS Fundamentals ### Enable RLS on Tables ```sql -- Enable RLS (required before policies take effect) ALTER TABLE users ENABLE ROW LEVEL SECURITY; ALTER TABLE posts ENABLE ROW LEVEL SECURITY; ALTER TABLE comments ENABLE ROW LEVEL SECURITY; -- Force RLS for table owners too (recommended) ALTER TABLE