kiss_firebase_repository_rest 0.3.0 copy "kiss_firebase_repository_rest: ^0.3.0" to clipboard
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:

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
);

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 #

  1. Install Firebase CLI (version 8.14 or higher):

    npm install -g firebase-tools
    
  2. 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:

  1. Initialize Firebase emulators in your project:

    firebase init emulators
    

    Select Firestore when prompted.

  2. Start the emulator:

    firebase emulators:start
    
  3. The emulator will be available at:

    • Firestore: http://127.0.0.1:8080
    • Emulator UI: http://127.0.0.1:4000

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 #

  1. Always use emulators for testing - never test against production
  2. Clear data between tests using TestUtils.clearEmulatorData()
  3. Check emulator status before running tests
  4. Use descriptive test names that explain what's being tested
  5. 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 ID
  • database: Database name (optional, defaults to "(default)")
  • firestore: FirestoreApi instance
  • toFirestore: Function to convert your model to Firestore Document
  • fromFirestore: Function to convert Firestore Document to your model
  • path: Collection path (e.g., 'users' or 'organizations/org1/users')
  • queryBuilder: Query builder for search operations
  • createId: Custom ID generator (optional)

Methods:

  • get(String id): Retrieve document by ID
  • add(IdentifiedObject<T> item): Add document with specific ID
  • addAutoIdentified(T item): Add document with auto-generated ID
  • update(String id, T Function(T) updater): Update existing document
  • delete(String id): Delete document
  • query({Query query}): Query multiple documents

RepositoryFirestoreJsonRestApi #

Specialized repository for JSON documents (schemaless).

Constructor Parameters:

  • projectId: Firebase project ID
  • firestore: FirestoreApi instance
  • path: Collection path
  • database: 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 #

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Write tests for your changes
  4. Ensure all tests pass (dart test)
  5. Commit your changes (git commit -m 'Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

License #

This project is licensed under the MIT License - see the LICENSE file for details.

0
likes
150
points
1.43k
downloads

Publisher

verified publisherwearemobilefirst.com

Weekly Downloads

A lightweight, type-safe Firestore implementation of the KISS Repository pattern for Dart using the Google Cloud Firestore REST API

Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

googleapis, googleapis_auth, http, kiss_repository, meta

More

Packages that depend on kiss_firebase_repository_rest