kiss_firebase_repository_rest 0.3.0
kiss_firebase_repository_rest: ^0.3.0 copied to clipboard
A lightweight, type-safe Firestore implementation of the KISS Repository pattern for Dart using the Google Cloud Firestore REST API
KISS Firebase Repository REST #
A lightweight, type-safe Firestore implementation of the KISS Repository pattern for Dart using the Google Cloud Firestore REST API.
Features #
- π― Type-safe: Generic repository with compile-time type safety
- π₯ Firebase REST API: Uses official Google Cloud Firestore REST API
- π¦ Lightweight: Minimal dependencies, focused on core functionality
- π§ͺ Testable: Comprehensive test suite with Firebase emulator support
- π CRUD Operations: Full Create, Read, Update, Delete support
- π Query Support: Built-in querying capabilities
- π Auto ID Generation: Automatic document ID generation
- π JSON Support: Built-in JSON document repository
Installation #
Add this to your package's pubspec.yaml
file:
dependencies:
kiss_firebase_repository_rest: ^0.1.0
Quick Start #
import 'package:kiss_firebase_repository_rest/kiss_firebase_repository_rest.dart';
// Choose an authentication method:
// Option 1: Service Account (for server/backend applications)
final googleClient = GoogleClient(
serviceAccountJson: serviceAccountJson,
scopes: ['https://www.googleapis.com/auth/datastore'], // Optional
);
// Option 2: Application Default Credentials (for Google Cloud environments)
final googleClient = GoogleClient.defaultCredentials(
scopes: ['https://www.googleapis.com/auth/datastore'],
);
// Option 3: OAuth2 User Consent (for CLI tools)
final googleClient = GoogleClient.userConsent(
clientId: 'your-client-id.apps.googleusercontent.com',
clientSecret: 'your-client-secret',
scopes: ['https://www.googleapis.com/auth/datastore'],
);
// Create Firestore client
final httpClient = await googleClient.getClient();
final firestore = FirestoreApi(httpClient);
// Create a repository for your model
final repository = RepositoryFirestoreRestApi<User>(
projectId: 'your-project-id',
database: null, // Uses default database
firestore: firestore,
toFirestore: (user, id) => Document(/* conversion logic */),
fromFirestore: (document) => User(/* conversion logic */),
path: 'users',
queryBuilder: YourQueryBuilder(),
);
// Use the repository
final user = User(name: 'John', email: 'john@example.com');
final addedUser = await repository.addAutoIdentified(user);
final retrievedUser = await repository.get(addedUser.id);
Authentication #
The GoogleClient
class supports three authentication methods:
1. Service Account (Recommended for servers) #
Best for backend services and applications running on servers.
final googleClient = GoogleClient(
serviceAccountJson: serviceAccountJson,
scopes: ['https://www.googleapis.com/auth/datastore'], // Optional, defaults to cloud-platform
);
2. Application Default Credentials (ADC) #
Best for applications running in Google Cloud environments (App Engine, Cloud Run, GKE, etc.).
final googleClient = GoogleClient.defaultCredentials(
scopes: ['https://www.googleapis.com/auth/datastore'],
);
When using ADC, credentials are automatically discovered from:
GOOGLE_APPLICATION_CREDENTIALS
environment variable- gcloud CLI credentials
- Google Cloud service account (when running on Google Cloud)
3. Unauthenticated (For emulators) #
Best for testing with Firestore emulators where authentication is not required.
final googleClient = GoogleClient.unauthenticated();
// Use with FirestoreApi configured for emulator endpoint
final firestore = FirestoreApi(
await googleClient.getClient(),
rootUrl: 'http://localhost:8080/', // Emulator URL
);
4. OAuth2 User Consent (For CLI tools) #
Best for command-line tools where users need to authenticate with their Google account.
final googleClient = GoogleClient.userConsent(
clientId: 'your-client-id.apps.googleusercontent.com',
clientSecret: 'your-client-secret',
scopes: ['https://www.googleapis.com/auth/datastore'],
);
Users will be prompted to visit a URL and grant permissions.
Custom Scopes #
All authentication methods support custom OAuth2 scopes. If not specified, the default scope is https://www.googleapis.com/auth/cloud-platform
.
Testing #
This package includes a comprehensive test suite that uses Firebase emulators for safe, isolated testing.
Prerequisites #
-
Install Firebase CLI (version 8.14 or higher):
npm install -g firebase-tools
-
Verify installation:
firebase --version
Quick Test Setup #
The tests now automatically start and manage Firebase emulators! Simply run:
# Run all tests (emulators will start automatically)
dart test
# Run only unit tests
dart test test/unit/
# Run only integration tests
dart test test/integration/
Automatic Emulator Management #
The test suite now features automatic emulator management:
- β Auto-start: Firebase emulators start automatically when tests run
- β Auto-stop: Emulators are stopped when tests complete
- β Error handling: Clear error messages if Firebase CLI is not installed
- β Smart detection: Skips startup if emulators are already running
Manual Emulator Setup (Optional) #
If you prefer to manage emulators manually:
-
Initialize Firebase emulators in your project:
firebase init emulators
Select Firestore when prompted.
-
Start the emulator:
firebase emulators:start
-
The emulator will be available at:
- Firestore:
http://127.0.0.1:8080
- Emulator UI:
http://127.0.0.1:4000
- Firestore:
Running Tests #
Run tests with automatic emulator management:
# Run all tests
dart test
# Run only unit tests
dart test test/unit/
# Run only integration tests
dart test test/integration/
# Run with expanded output showing each test
dart test --reporter=expanded
Test Structure #
test/
βββ unit/ # Unit tests
β βββ repository_firestore_rest_api_test.dart
βββ integration/ # Integration tests
β βββ json_repository_test.dart
βββ test_models.dart # Test data models
βββ test_utils.dart # Test utilities
βββ emulator_test_runner.dart # Automated test runner
Test Utilities #
The test suite includes helper utilities with automatic emulator management:
import 'package:test/test.dart';
import 'emulator_test_runner.dart';
import 'test_utils.dart';
void main() {
group('My Tests', () {
setUpAll(() async {
// Auto-start Firebase emulator if not running
await EmulatorTestRunner.startEmulator();
});
tearDownAll(() async {
// Stop emulator after all tests complete
await EmulatorTestRunner.stopEmulator();
});
setUp(() async {
// Clear test data before each test
await TestUtils.clearEmulatorData();
});
test('should work with emulator', () async {
final repository = await TestUtils.createUserRepository();
// Your test code here
});
});
}
Automated Test Runner #
Use the built-in test runner for automatic emulator management:
// Run this to check your test environment
dart run test/emulator_test_runner.dart
Test Features #
- β Emulator Integration: Tests run against Firebase emulator
- β Data Isolation: Each test gets a clean database state
- β CRUD Testing: Comprehensive Create, Read, Update, Delete tests
- β Error Handling: Tests for various error conditions
- β Type Safety: Tests with strongly-typed models
- β JSON Support: Tests for JSON document operations
- β Edge Cases: Tests for special characters, large data, etc.
- β Concurrent Operations: Tests for concurrent modifications
Testing Best Practices #
- Always use emulators for testing - never test against production
- Clear data between tests using
TestUtils.clearEmulatorData()
- Check emulator status before running tests
- Use descriptive test names that explain what's being tested
- Test both happy path and error conditions
Troubleshooting Tests #
Emulator not starting:
# Check if ports are in use
lsof -i :8080
lsof -i :4000
# Kill processes using the ports
kill -9 <PID>
# Restart emulator
firebase emulators:start
Tests failing with connection errors:
// Verify emulator connectivity
if (!await TestUtils.isEmulatorRunning()) {
print('Emulator is not running!');
}
// Check firestore configuration
final firestore = await TestUtils.createEmulatorFirestoreApi();
Invalid reporter option errors:
# Use valid reporter options
dart test --reporter=expanded # Detailed output
dart test --reporter=compact # Default, concise output
dart test --reporter=github # For CI/CD environments
Permission errors:
- Ensure your service account has proper permissions
- For emulator testing, mock credentials are used automatically
Port conflicts:
# Check what's using the ports
lsof -i :8080
lsof -i :4000
# Kill processes if needed
kill -9 <PID>
Usage Examples #
Basic User Repository #
class User {
final String id;
final String name;
final String email;
final int? age;
User({required this.id, required this.name, required this.email, this.age});
}
// Convert User to Firestore Document
Document userToFirestore(User user, String? id) {
return RepositoryFirestoreRestApi.fromJson(
json: {
'name': user.name,
'email': user.email,
if (user.age != null) 'age': user.age,
},
id: id,
);
}
// Convert Firestore Document to User
User userFromFirestore(Document document) {
final json = RepositoryFirestoreRestApi.toJson(document);
return User(
id: document.name?.split('/').last ?? '',
name: json['name'] as String,
email: json['email'] as String,
age: json['age'] as int?,
);
}
// Create repository
final userRepository = RepositoryFirestoreRestApi<User>(
projectId: 'my-project',
firestore: firestore,
toFirestore: userToFirestore,
fromFirestore: userFromFirestore,
path: 'users',
queryBuilder: CollectionQueryBuilder(collectionId: 'users'),
);
JSON Repository (Schemaless) #
final jsonRepository = RepositoryFirestoreJsonRestApi(
projectId: 'my-project',
firestore: firestore,
path: 'documents',
);
// Add any JSON data
await jsonRepository.add(IdentifiedObject('doc1', {
'title': 'My Document',
'content': 'Some content',
'tags': ['important', 'draft'],
'metadata': {
'author': 'John Doe',
'created': DateTime.now().toIso8601String(),
}
}));
Advanced Querying #
// Custom query builder (implement your own logic)
class CustomQueryBuilder implements QueryBuilder<RunQueryRequest> {
@override
RunQueryRequest build(Query query) {
// Implement custom query logic based on your needs
return RunQueryRequest(
structuredQuery: StructuredQuery(
from: [CollectionSelector(collectionId: 'users')],
// Add filters, ordering, limits, etc.
),
);
}
}
API Reference #
RepositoryFirestoreRestApi #
Main repository class for typed operations.
Constructor Parameters:
projectId
: Firebase project IDdatabase
: Database name (optional, defaults to "(default)")firestore
: FirestoreApi instancetoFirestore
: Function to convert your model to Firestore DocumentfromFirestore
: Function to convert Firestore Document to your modelpath
: Collection path (e.g., 'users' or 'organizations/org1/users')queryBuilder
: Query builder for search operationscreateId
: Custom ID generator (optional)
Methods:
get(String id)
: Retrieve document by IDadd(IdentifiedObject<T> item)
: Add document with specific IDaddAutoIdentified(T item)
: Add document with auto-generated IDupdate(String id, T Function(T) updater)
: Update existing documentdelete(String id)
: Delete documentquery({Query query})
: Query multiple documents
RepositoryFirestoreJsonRestApi #
Specialized repository for JSON documents (schemaless).
Constructor Parameters:
projectId
: Firebase project IDfirestore
: FirestoreApi instancepath
: Collection pathdatabase
: Database name (optional)queryBuilder
: Query builder (optional)
Error Handling #
The package throws RepositoryException
for various error conditions:
try {
final user = await repository.get('non-existent-id');
} on RepositoryException catch (e) {
switch (e.code) {
case RepositoryErrorCode.notFound:
print('User not found');
break;
case RepositoryErrorCode.alreadyExists:
print('User already exists');
break;
default:
print('Other error: ${e.message}');
}
}
Performance Considerations #
- Use batch operations when possible (planned for future releases)
- Implement proper indexing in Firestore for query performance
- Consider pagination for large result sets
- Use subcollections for hierarchical data organization
Contributing #
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Write tests for your changes
- Ensure all tests pass (
dart test
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
License #
This project is licensed under the MIT License - see the LICENSE file for details.