suparepo 1.4.6
suparepo: ^1.4.6 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)
- 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.3.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>>();
}
}
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?,
);
}
}
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]
# 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