suparepo 1.11.3 copy "suparepo: ^1.11.3" to clipboard
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_proc catalog 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)

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 setof prefix to generate a method returning List<T>
  • These overrides take the highest priority over all other detection methods
  • No need to create an execute_sql function — 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 DEFINER privileges. Ensure it is only callable with the service_role key (which suparepo uses via secret_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_runner after generation to create the .freezed.dart part files. Requires execute_sql RPC 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:

  • !field check present → required
  • typeof 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_types is enabled by default. Functions with YAML models definitions take precedence; only undefined functions are inferred from TypeScript. Set auto_detect_types: false to 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 error field string literals in snake_case format
  • 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 where supabase_client_provider.dart is written
  • client_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:

  1. Generate models with supafreeze
  2. Set model_import_prefix (or model_import_path) in suparepo.yaml to point to your models
  3. Optionally enable generate_providers: true for Riverpod DI
  4. Generate repositories with suparepo

This gives you fully type-safe repositories with Freezed models and optional Riverpod providers.

License #

MIT

0
likes
160
points
625
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Generate repository/data access layer code from Supabase database schema. Automatically creates CRUD operations, queries, and type-safe API clients.

Homepage
Repository (GitHub)
View/report issues

Topics

#supabase #repository #code-generation #data-access #postgresql

License

MIT (license)

Dependencies

http, path, recase, supabase_schema_core, yaml

More

Packages that depend on suparepo