Functions-overview
Clean, type-safe Firebase Callable Functions integration with Either-based error handling.
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});
}
}
final function = MyFunction();
final result = await function.call(param: 'value');
result.fold(
(error) => print('Error: ${error.message}'),
(data) => print('Success: ${data}'),
);
- AppFunctionService - Handles Firebase callable execution and response parsing
- AppFunction<E, R> - Base class for all functions with Either pattern
- ErrorSchema - Standardized error response from backend
E: Error type (typicallyErrorSchema)R: Success response type (your schema class)
All functions return Either<ErrorSchema, ResponseSchema>:
- Left(error): Function failed with structured error
- Right(data): Function succeeded with typed data
✅ 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
ErrorSchema(
endpoint: 'function_name',
code: AppErrorCode.serverError,
category: AppErrorCategory.server,
title: 'Error Title',
message: 'Detailed error message',
cause: 'Optional cause string',
)
database- Database errorssearch- Search errorsauth- Authentication errorsvalidation- Validation errorsstripe- Payment/Stripe errorsserver- Internal server errors- And more…
Each category has specific error codes (see AppErrorCode):
serverError,databaseError,maintenancestripeInvalidUser,stripeCardError, etc.- Full list in auto-generated enum
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),
);
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']}');
}
},
);
final function = StripeRefreshAccountFunction();
final result = await function.refresh(
uid: 'user123',
stripeAccountId: 'acct_123',
accountType: StripeAccountType.express,
);
result.fold(
(error) => handleStripeError(error),
(data) => updateAccountUI(data),
);
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);
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);
Functions receive parameters as-is from execute(params: {...}).
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"
}
}
The service automatically maps Firebase error codes:
invalid-argument→INVALID_ARGUMENTunauthenticated→UNAUTHENTICATEDpermission-denied→PERMISSION_DENIEDnot-found→NOT_FOUNDdeadline-exceeded→TIMEOUT- And more…
class MyFunction extends AppFunction<MyCustomError, MyResponse> {
@override
MyCustomError Function(JsonDef)? get errorFromJson =>
MyCustomError.fromJson;
// ...
}
final result1 = await function1.execute();
final result2 = result1.flatMap((data1) =>
function2.execute(params: data1.toJson())
);
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),
);
- ARCHITECTURE_PROPOSAL.md - Design details and rationale
- MIGRATION_GUIDE.md - Migrate from old pattern
- EXAMPLE_USAGE.dart - Comprehensive code examples
fpdart- Functional programming with Either typecloud_functions- Firebase callable functionsstore- 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