Firebase Functions Package

Clean, type-safe Firebase Callable Functions integration with Either-based error handling.

Quick Start

1. Define Your Function

import 'package:fpdart/fpdart.dart' show Either;
import 'package:services/function/app_function.dart';
import 'package:store/function/schema/error_schema.dart';
import 'package:store/function/schema/my_response_schema.dart';

class MyFunction extends AppFunction<ErrorSchema, MyResponseSchema> {
  const MyFunction({super.service});

  @override
  String get name => 'my_firebase_function_name';

  @override
  MyResponseSchema Function(JsonDef) get fromJson => MyResponseSchema.fromJson;

  Future<Either<ErrorSchema, MyResponseSchema>> call({
    required String param,
  }) {
    return execute(params: {'param': param});
  }
}

2. Use Your Function

final function = MyFunction();
final result = await function.call(param: 'value');

result.fold(
  (error) => print('Error: ${error.message}'),
  (data) => print('Success: ${data}'),
);

Architecture

Core Components

  1. AppFunctionService - Handles Firebase callable execution and response parsing
  2. AppFunction<E, R> - Base class for all functions with Either pattern
  3. ErrorSchema - Standardized error response from backend

Type Parameters

  • E: Error type (typically ErrorSchema)
  • R: Success response type (your schema class)

Result Type

All functions return Either<ErrorSchema, ResponseSchema>:

  • Left(error): Function failed with structured error
  • Right(data): Function succeeded with typed data

Features

Type Safety - Compile-time guarantees for request/response types
Error Handling - Structured errors with categories and codes
Firebase Integration - Automatic error code mapping
Testability - Easy to mock and unit test
Composability - Chain operations with flatMap/map
Documentation - Self-documenting with typed schemas

Error Handling

Error Structure

ErrorSchema(
  endpoint: 'function_name',
  code: AppErrorCode.serverError,
  category: AppErrorCategory.server,
  title: 'Error Title',
  message: 'Detailed error message',
  cause: 'Optional cause string',
)

Error Categories

  • database - Database errors
  • search - Search errors
  • auth - Authentication errors
  • validation - Validation errors
  • stripe - Payment/Stripe errors
  • server - Internal server errors
  • And more…

Error Codes

Each category has specific error codes (see AppErrorCode):

  • serverError, databaseError, maintenance
  • stripeInvalidUser, stripeCardError, etc.
  • Full list in auto-generated enum

Handling Errors

result.fold(
  (error) {
    // Check category
    if (error.category == AppErrorCategory.auth) {
      redirectToLogin();
      return;
    }
    
    // Check specific code
    if (error.code == AppErrorCode.maintenance) {
      showMaintenanceScreen();
      return;
    }
    
    // Show generic error
    showError(error.title, error.message);
  },
  (data) => processSuccess(data),
);

Examples

Search Function

final searchFn = SearchFunction.general(
  searchType: SearchFunctionType.dare,
  sortType: SearchSortType.recent,
);

final result = await searchFn.search(queryStr: 'flutter', pageNum: 1);
result.fold(
  (error) => showError(error.message),
  (data) {
    print('Found ${data.found} results');
    for (final item in data.results) {
      print('- ${item['title']}');
    }
  },
);

Stripe Function

final function = StripeRefreshAccountFunction();
final result = await function.refresh(
  uid: 'user123',
  stripeAccountId: 'acct_123',
  accountType: StripeAccountType.express,
);

result.fold(
  (error) => handleStripeError(error),
  (data) => updateAccountUI(data),
);

Testing

Mock Success

final mockService = MockAppFunctionService();
when(mockService.executeCallable<MySchema>(
  functionName: 'my_function',
  fromJson: any,
  args: any,
)).thenAnswer((_) async => Right(successData));

final function = MyFunction.custom(service: mockService);
final result = await function.call();

expect(result.isRight(), true);

Mock Error

when(mockService.executeCallable<MySchema>(
  functionName: 'my_function',
  fromJson: any,
  args: any,
)).thenAnswer((_) async => Left(errorSchema));

final function = MyFunction.custom(service: mockService);
final result = await function.call();

expect(result.isLeft(), true);

Backend Contract

Request Format

Functions receive parameters as-is from execute(params: {...}).

Response Format

Backend must return:

Success:

{
  "status": "success",
  "data": {
    // Your response schema
  }
}

Failure:

{
  "status": "failure",
  "error": {
    "endpoint": "function_name",
    "code": "error_code",
    "category": "error_category",
    "title": "Error Title",
    "message": "Error message",
    "cause": "Optional cause"
  }
}

Firebase Error Codes

The service automatically maps Firebase error codes:

  • invalid-argumentINVALID_ARGUMENT
  • unauthenticatedUNAUTHENTICATED
  • permission-deniedPERMISSION_DENIED
  • not-foundNOT_FOUND
  • deadline-exceededTIMEOUT
  • And more…

Advanced Usage

Custom Error Types

class MyFunction extends AppFunction<MyCustomError, MyResponse> {
  @override
  MyCustomError Function(JsonDef)? get errorFromJson => 
    MyCustomError.fromJson;
  // ...
}

Chaining Operations

final result1 = await function1.execute();
final result2 = result1.flatMap((data1) => 
  function2.execute(params: data1.toJson())
);

Error Recovery

final result = await function.execute();
final recovered = result.fold(
  (error) async {
    if (error.code.name == 'timeout') {
      return await retryFunction();
    }
    return Either.left(error);
  },
  (data) async => Either.right(data),
);

Documentation

Package Dependencies

  • fpdart - Functional programming with Either type
  • cloud_functions - Firebase callable functions
  • store - Schema definitions (ErrorSchema, response schemas)
  • Schema definitions: packages/store/lib/function/schema/
  • Error codes: packages/store/lib/function/shared/app_error_code.dart
  • Error categories: packages/store/lib/function/shared/app_error_category.dart