suparepo 1.16.0 copy "suparepo: ^1.16.0" to clipboard
suparepo: ^1.16.0 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.

Automatic JSON Schema Detection (RETURNS json/jsonb) #

For functions that use RETURNS json or RETURNS jsonb, suparepo automatically parses json_build_object() / jsonb_build_object() calls in the function body to detect field names and infer types from variable declarations.

No configuration needed — just enable generate_result_models:

rpc:
  enabled: true
  generate_result_models: true

Example: For a SQL function:

CREATE FUNCTION get_membership_rank_info(p_user_id uuid)
RETURNS json AS $$
DECLARE
  v_rank text;
  v_upload_days int4;
  v_is_active bool;
BEGIN
  -- ... logic ...
  RETURN json_build_object(
    'rank', v_rank,
    'upload_days', v_upload_days,
    'is_active', v_is_active
  );
END;
$$ LANGUAGE plpgsql;

suparepo automatically detects the fields (rank: text, upload_days: int4, is_active: bool) and generates the same Freezed model as RETURNS TABLE functions.

Supported patterns:

  • Variable references with DECLARE types: 'key', v_variable
  • Type cast expressions: 'key', expr::int4
  • json_build_object() and jsonb_build_object()

Note: Requires execute_sql RPC function for pg_proc.prosrc access. If auto-detection fails or the function doesn't use json_build_object, use YAML-defined result_models as a fallback.

Error Code Sealed Class Generation #

For RETURNS TABLE(success bool, error text) functions, suparepo automatically detects error code string literals from the PL/pgSQL function body and generates a Freezed sealed class.

Example SQL:

CREATE FUNCTION execute_exchange_atomic(p_mst_id int)
RETURNS TABLE(success bool, error text) AS $$
BEGIN
  IF check_daily_limit() THEN
    return query select false, 'daily_limit_exceeded'::text;
  END IF;
  IF check_balance() THEN
    return query select false, 'insufficient_balance'::text;
  END IF;
  return query select true, ''::text;
END;
$$ LANGUAGE plpgsql;

Generated error class (execute_exchange_atomic_error.dart):

@freezed
sealed class ExecuteExchangeAtomicError
    with _$ExecuteExchangeAtomicError {
  const ExecuteExchangeAtomicError._();

  const factory ExecuteExchangeAtomicError.dailyLimitExceeded()
      = ExecuteExchangeAtomicErrorDailyLimitExceeded;
  const factory ExecuteExchangeAtomicError.insufficientBalance()
      = ExecuteExchangeAtomicErrorInsufficientBalance;
  const factory ExecuteExchangeAtomicError.unknown({
    required String code,
  }) = ExecuteExchangeAtomicErrorUnknown;

  factory ExecuteExchangeAtomicError.fromErrorCode(
      String code) { ... }
}

Generated result model uses the error type:

@freezed
abstract class ExecuteExchangeAtomicResult ... {
  const factory ExecuteExchangeAtomicResult({
    required bool success,
    required ExecuteExchangeAtomicError error,  // typed!
  }) = _ExecuteExchangeAtomicResult;
}

Supported PL/pgSQL patterns:

  • return query select false, 'error_code'::text;
  • error := 'error_code';

Note: Only functions with an error text (or error_code text) column in RETURNS TABLE are analyzed. Requires execute_sql RPC function.

YAML-Defined Result Models (Manual Override) #

For functions where auto-detection doesn't work (e.g. no json_build_object, or you want to override the detected schema), you can define the column schema in suparepo.yaml:

rpc:
  enabled: true
  generate_result_models: true
  result_models:
    get_membership_rank_info:
      rank: { type: text }
      upload_days: { type: int4 }
      is_active: { type: bool }
    get_user_profile:
      name: text          # shorthand syntax
      avatar_url: text

Example: For a SQL function:

CREATE FUNCTION get_membership_rank_info(p_user_id uuid)
RETURNS json AS $$
BEGIN
  RETURN json_build_object(
    'rank', v_rank,
    'upload_days', v_upload_days,
    'is_active', v_is_active
  );
END;
$$ LANGUAGE plpgsql;

suparepo generates:

// get_membership_rank_info_result.dart
@freezed
abstract class GetMembershipRankInfoResult
    with _$GetMembershipRankInfoResult {
  const factory GetMembershipRankInfoResult({
    required String rank,
    required int uploadDays,
    required bool isActive,
  }) = _GetMembershipRankInfoResult;

  factory GetMembershipRankInfoResult.fromRow(
    Map<String, dynamic> row,
  ) => GetMembershipRankInfoResult(
    rank: row['rank'] as String,
    uploadDays: row['upload_days'] as int,
    isActive: row['is_active'] as bool,
  );
}

And the RPC client returns a single typed object:

Future<GetMembershipRankInfoResult> getMembershipRankInfo({
  required String pUserId,
}) async {
  final response = await _client.rpc<Map<String, dynamic>>(
    'get_membership_rank_info',
    params: {'p_user_id': pUserId},
  );
  return GetMembershipRankInfoResult.fromRow(response);
}

Note: YAML result_models definitions take precedence over auto-detected RETURNS TABLE columns, allowing you to override the schema if needed.

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)
  result_models:                # YAML-defined schemas for RETURNS json/jsonb functions
    get_membership_rank_info:
      rank: { type: text }
      upload_days: { type: int4 }
      is_active: { type: bool }

# 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
495
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, supa_query_annotation, supabase_schema_core, yaml

More

Packages that depend on suparepo