suparepo 1.11.3
suparepo: ^1.11.3 copied to clipboard
Generate repository/data access layer code from Supabase database schema. Automatically creates CRUD operations, queries, and type-safe API clients.
suparepo #
Generate repository, RPC client, and Edge Function client code from Supabase automatically.
Features #
- Repository generation — CRUD operations (getAll, getById, create, update, delete), pagination, count, relation queries
- RPC client generation — Type-safe Dart methods from Supabase SQL functions (auto-detected via OpenAPI spec, with
pg_proccatalog correction for accurate return types) - Edge Function client generation — Typed or untyped clients from local
supabase/functions/directory, with automatic TypeScript type inference - Type-safe when used with supafreeze models
Installation #
dependencies:
suparepo: ^1.8.0
Quick Start #
1. Create Configuration #
Create suparepo.yaml in your project root:
url: ${SUPABASE_DATA_API_URL}
secret_key: ${SUPABASE_SECRET_KEY}
output: lib/repositories
# Optional: Link to model classes (generated by supafreeze)
# Use model_import_path for a barrel file import:
model_import_path: package:myapp/models/models.dart
# Or use model_import_prefix for individual file imports (recommended):
# model_import_prefix: package:myapp/
# Optional: Generate barrel file (default: false)
generate_barrel: false
# Optional: Generate Riverpod providers (default: false)
generate_providers: true
# Optional: Filter tables
include:
- users
- posts
2. Set Environment Variables #
Create .env file:
SUPABASE_DATA_API_URL=https://your-project.supabase.co
SUPABASE_SECRET_KEY=your-service-role-key
3. Run Generator #
dart run suparepo
CLI Options #
dart run suparepo # Generate all enabled targets
dart run suparepo --repo # Generate table repositories only
dart run suparepo --rpc # Generate RPC client only
dart run suparepo --edge # Generate Edge Function client only
dart run suparepo --force # Force regenerate all
RPC Client Generation #
Automatically generates type-safe Dart methods for your Supabase SQL functions (RPC).
Configuration #
rpc:
enabled: true
output: lib/repositories/rpc_client.dart # optional
include:
- get_user_posts
- search_users
exclude:
- internal_cleanup
Generated Code Example #
For a SQL function get_user_posts(user_id uuid) returning setof json:
class SupabaseRpcClient {
final SupabaseClient _client;
const SupabaseRpcClient(this._client);
Future<List<Map<String, dynamic>>> getUserPosts({
required String userId,
}) async {
final response = await _client.rpc('get_user_posts', params: {
'user_id': userId,
});
return (response as List).cast<Map<String, dynamic>>();
}
}
Return Type Correction #
PostgREST's OpenAPI spec does not always return accurate type information for RPC functions with scalar return values (boolean, integer, text, etc.) or RETURNS TABLE(...) functions. As a result, methods that should be Future<bool>, Future<String>, or Future<List<Map<String, dynamic>>> may be generated as Future<void>.
suparepo resolves return types in the following priority order:
1. YAML return_types (highest priority, manual)
↓ not defined
2. pg_proc auto-correction (when execute_sql exists)
↓ execute_sql not available
3. OpenAPI spec (fallback, may produce void)
Option 1: Manual override with return_types (recommended)
The simplest approach. Specify return types directly in suparepo.yaml:
rpc:
enabled: true
return_types:
get_my_invite_code: text
is_active_user: bool
count_items: int4
execute_exchange_atomic: void
get_favorite_products: setof jsonb
- Values are PostgreSQL type names (
text,bool,int4,int8,jsonb,uuid, etc.) - Add
setofprefix to generate a method returningList<T> - These overrides take the highest priority over all other detection methods
- No need to create an
execute_sqlfunction — this is the easiest option
Type mapping reference:
| PostgreSQL type | Dart type |
|---|---|
text, varchar |
String |
bool |
bool |
int4, int8 |
int |
float4, float8 |
double |
jsonb, json |
Map<String, dynamic> |
uuid |
String |
timestamptz |
DateTime |
void |
void |
Option 2: Automatic correction via execute_sql
By creating an execute_sql RPC function in your Supabase project, suparepo can query the PostgreSQL pg_proc catalog directly to auto-detect accurate return types for all RPC functions.
Why is this needed? PostgREST's OpenAPI spec does not always report RPC return types accurately. The execute_sql function allows suparepo to run a SQL query against pg_proc (PostgreSQL's internal function definition table) to retrieve the correct type information.
Run the following in Supabase SQL Editor:
CREATE OR REPLACE FUNCTION execute_sql(query text)
RETURNS json
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
result json;
BEGIN
EXECUTE query INTO result;
RETURN result;
END;
$$;
Warning: This function runs with
SECURITY DEFINERprivileges. Ensure it is only callable with the service_role key (which suparepo uses viasecret_key). Review your RLS policies and API exposure settings to prevent unauthorized access.
Once created, no additional configuration is needed — suparepo automatically detects execute_sql and applies corrections. Functions using RETURNS TABLE(...) are automatically generated as Future<List<Map<String, dynamic>>>. If execute_sql does not exist, no error is raised and the OpenAPI spec results are used as-is.
Using both return_types and execute_sql: When both are configured, functions listed in return_types use the YAML values, while all other functions are corrected via execute_sql.
Freezed Result Model Generation #
For RETURNS TABLE(...) functions, suparepo can automatically generate Freezed result model classes instead of returning List<Map<String, dynamic>>.
Configuration:
rpc:
enabled: true
generate_result_models: true
result_models_output: lib/models/rpc # optional (default: same directory as rpc client)
Example: For a SQL function:
CREATE FUNCTION get_my_invite_code(user_id uuid)
RETURNS TABLE(code text, max_invites int4, is_active bool)
suparepo generates a Freezed model (get_my_invite_code_result.dart):
@freezed
abstract class GetMyInviteCodeResult with _$GetMyInviteCodeResult {
const factory GetMyInviteCodeResult({
required String code,
required int maxInvites,
required bool isActive,
}) = _GetMyInviteCodeResult;
factory GetMyInviteCodeResult.fromRow(Map<String, dynamic> row) =>
GetMyInviteCodeResult(
code: row['code'] as String,
maxInvites: row['max_invites'] as int,
isActive: row['is_active'] as bool,
);
}
And the RPC client method returns the typed model:
Future<List<GetMyInviteCodeResult>> getMyInviteCode({
required String userId,
}) async {
final response = await _client.rpc<List<dynamic>>('get_my_invite_code',
params: {'user_id': userId});
return response.cast<Map<String, dynamic>>()
.map(GetMyInviteCodeResult.fromRow).toList();
}
Note: Run
build_runnerafter generation to create the.freezed.dartpart files. Requiresexecute_sqlRPC function for TABLE column detection.
Edge Function Client Generation #
Generates client code for your Supabase Edge Functions by scanning the local supabase/functions/ directory.
Automatic TypeScript Type Inference #
suparepo analyzes your Edge Function TypeScript source code and automatically infers request/response types. This eliminates the need to manually define models in YAML.
Supported TypeScript patterns:
// Request: extracted from `body as { ... }` pattern
const { amount, provider } = body as {
amount?: number;
provider?: string;
};
// Required detection: inferred from validation if-statements
if (!amount || !provider) {
return new Response(JSON.stringify({ error: "Missing" }), { status: 400 });
}
// Response: extracted from success response (status 2xx) JSON.stringify()
return new Response(
JSON.stringify({ exchange_id: data.id, message: "OK" }),
{ status: 200, headers },
);
Type mapping:
| TypeScript | Dart |
|---|---|
string |
String |
number |
int |
boolean |
bool |
Required/optional detection:
!fieldcheck present → requiredtypeof field !== "type"check present → required?suffix with no validation → optional
Configuration #
Minimal configuration (types are auto-detected from TypeScript):
edge_functions:
enabled: true
functions_path: supabase/functions
Full configuration:
edge_functions:
enabled: true
functions_path: supabase/functions # default
output: lib/repositories/edge_function_client.dart # optional
auto_detect_types: true # Auto-detect types from TypeScript (default: true)
include:
- send-email
exclude:
- hello
# Functions with YAML model definitions take precedence over auto-detection
models:
send-email:
request:
to: { type: text, required: true }
subject: { type: text, required: true }
body_html: { type: text }
response:
success: { type: bool, required: true }
message_id: { type: text }
Note:
auto_detect_typesis enabled by default. Functions with YAMLmodelsdefinitions take precedence; only undefined functions are inferred from TypeScript. Setauto_detect_types: falseto disable.
Generated Code — Without Type Definitions #
When auto_detect_types: false and no models defined:
class SupabaseEdgeFunctionClient {
final SupabaseClient _client;
const SupabaseEdgeFunctionClient(this._client);
Future<FunctionResponse> sendEmail({
Map<String, dynamic>? body,
Map<String, String>? headers,
}) async {
return await _client.functions.invoke(
'send-email', body: body, headers: headers,
);
}
}
Generated Code — With Type Definitions #
When types are auto-detected or defined via YAML, typed request/response classes are generated:
class SendEmailRequest {
final String to;
final String subject;
final String? bodyHtml;
const SendEmailRequest({
required this.to,
required this.subject,
this.bodyHtml,
});
Map<String, dynamic> toJson() => {
'to': to,
'subject': subject,
if (bodyHtml != null) 'body_html': bodyHtml,
};
}
class SendEmailResponse {
final bool success;
final String? messageId;
const SendEmailResponse({required this.success, this.messageId});
factory SendEmailResponse.fromJson(Map<String, dynamic> json) {
return SendEmailResponse(
success: json['success'] as bool,
messageId: json['message_id'] as String?,
);
}
}
Error Type Generation (Freezed sealed class) #
suparepo automatically detects error responses from your Edge Function TypeScript source and generates Freezed sealed classes for type-safe error handling.
Detected TypeScript pattern:
// Edge Function: create-order
if (!hasStock) {
return new Response(
JSON.stringify({ error: "out_of_stock", message: "Item is out of stock" }),
{ status: 400, headers },
);
}
if (duplicateOrder) {
return new Response(
JSON.stringify({ error: "duplicate_order", message: "Already ordered" }),
{ status: 409, headers },
);
}
- Scans
new Response(...)blocks with status 4xx/5xx - Extracts
errorfield string literals insnake_caseformat - Skips generic messages (e.g.,
"Missing","Bad request")
Generated code (create_order_error.dart):
@freezed
sealed class CreateOrderError with _$CreateOrderError {
const CreateOrderError._();
const factory CreateOrderError.outOfStock({
required int status,
required String message,
}) = CreateOrderErrorOutOfStock;
const factory CreateOrderError.duplicateOrder({
required int status,
required String message,
}) = CreateOrderErrorDuplicateOrder;
const factory CreateOrderError.unknown({
required int status,
required String errorCode,
required String message,
}) = CreateOrderErrorUnknown;
factory CreateOrderError.fromFunctionException(
FunctionException e,
) {
final body = jsonDecode(e.details as String)
as Map<String, dynamic>;
final code = body['error'] as String? ?? '';
final msg = body['message'] as String? ?? code;
return switch (code) {
'out_of_stock' => CreateOrderError.outOfStock(
status: e.status, message: msg),
'duplicate_order' => CreateOrderError.duplicateOrder(
status: e.status, message: msg),
_ => CreateOrderError.unknown(
status: e.status, errorCode: code, message: msg),
};
}
}
Usage in your app:
try {
await edgeFunctionClient.createOrder(request: req);
} on FunctionException catch (e) {
final error = CreateOrderError.fromFunctionException(e);
switch (error) {
case CreateOrderErrorOutOfStock(:final message):
showSnackBar(message);
case CreateOrderErrorDuplicateOrder():
showSnackBar('Already ordered');
case CreateOrderErrorUnknown(:final errorCode):
showSnackBar('Error: $errorCode');
}
}
Error class files are generated alongside the Edge Function client in the same directory. Run build_runner to generate the .freezed.dart part files.
Repository Generation #
Generated Code Example #
For a users table with columns id, email, name:
class UsersRepository {
final SupabaseClient _client;
UsersRepository(this._client);
Future<List<User>> getAll() async {
final response = await _client.from('users').select();
return response.map((e) => User.fromJson(e)).toList();
}
Future<User?> getById(String id) async {
final response = await _client
.from('users')
.select()
.eq('id', id)
.maybeSingle();
return response != null ? User.fromJson(response) : null;
}
Future<User> create(User data) async { ... }
Future<User> update(String id, User data) async { ... }
Future<void> delete(String id) async { ... }
Future<int> count() async { ... }
Future<List<User>> paginate({int page = 1, int perPage = 20}) async { ... }
}
Riverpod Provider Generation #
When generate_providers: true is set, each repository and RPC client gets a @Riverpod(keepAlive: true) provider:
@Riverpod(keepAlive: true)
UsersRepository usersRepository(Ref ref) {
final client = ref.watch(supabaseClientProvider);
return UsersRepository(client);
}
A supabase_client_provider.dart is also generated, which must be overridden in your ProviderScope:
@Riverpod(keepAlive: true)
SupabaseClient supabaseClient(Ref ref) {
throw UnimplementedError(
'supabaseClientProvider must be overridden in ProviderScope.',
);
}
Custom Provider Output Path
By default, supabase_client_provider.dart is generated in the same directory as repositories. To output it to a different location (e.g., a Gateway package), use client_provider_output and client_provider_import:
generate_providers: true
client_provider_output: ../gateway/lib/supabase/supabase_client_provider.dart
client_provider_import: package:gateway/supabase/supabase_client_provider.dart
client_provider_output— File path wheresupabase_client_provider.dartis writtenclient_provider_import— Import path used in generated repository/RPC code to reference the provider
Model Import Options #
There are two ways to link generated repositories to supafreeze models:
Barrel file import (model_import_path):
model_import_path: package:myapp/models/models.dart
All repositories import from a single barrel file.
Individual file import (model_import_prefix, recommended):
model_import_prefix: package:myapp/
Each repository imports its own model file (e.g., package:myapp/users.supafreeze.dart). Takes precedence over model_import_path.
Full Configuration Reference #
url: ${SUPABASE_DATA_API_URL}
secret_key: ${SUPABASE_SECRET_KEY}
output: lib/repositories
schema: public
fetch: always # always | if_no_cache | never
generate_barrel: false # Generate barrel file for repositories (default: false)
generate_providers: true # Generate Riverpod providers (default: false)
client_provider_output: ../gateway/lib/supabase/supabase_client_provider.dart # Custom output path
client_provider_import: package:gateway/supabase/supabase_client_provider.dart # Custom import path
model_import_path: package:myapp/models/models.dart # Barrel file import
model_import_prefix: package:myapp/ # Individual file import (takes precedence)
supabase_import: package:supabase_flutter/supabase_flutter.dart # or package:supabase/supabase.dart for pure Dart
# Table filter (for repository generation)
include: [users, posts]
# exclude: [_migrations]
# RPC function client
rpc:
enabled: true
output: lib/repositories/rpc_client.dart
include: [get_user_posts]
exclude: [internal_cleanup]
return_types: # Manual return type overrides (see "Return Type Correction")
get_invite_code: text
is_active: bool
get_items: setof jsonb
generate_result_models: true # Generate Freezed result models for RETURNS TABLE functions (default: false)
result_models_output: lib/models/rpc # Custom output directory for result models (optional)
# Edge Function client
edge_functions:
enabled: true
output: lib/repositories/edge_function_client.dart
functions_path: supabase/functions
auto_detect_types: true # Auto-detect types from TypeScript (default: true)
include: [send-email]
# exclude: [hello]
# Functions with YAML models take precedence; others are auto-detected from TS
models:
send-email:
request:
to: { type: text, required: true }
subject: { type: text, required: true }
response:
success: { type: bool, required: true }
Using with supafreeze #
For the best experience, use suparepo together with supafreeze:
- Generate models with supafreeze
- Set
model_import_prefix(ormodel_import_path) in suparepo.yaml to point to your models - Optionally enable
generate_providers: truefor Riverpod DI - Generate repositories with suparepo
This gives you fully type-safe repositories with Freezed models and optional Riverpod providers.
License #
MIT