Skip to content

Lightweight Cloud Firestore Client API using googleapis gRPC.

License

Notifications You must be signed in to change notification settings

1amageek/FirebaseAPI

Repository files navigation

FirebaseAPI

FirebaseAPI for Swift is a Swift package that provides a simple interface to interact with Firebase services using gRPC.

This repository includes the googleapis repository as a submodule, which is used to generate the API client code for Firebase.

Features

  • Firestore API: Full support for Firestore operations (CRUD, queries, transactions, batches)
  • Generic Transport: Support for any ClientTransport implementation from grpc-swift-2
  • Swift 6 Ready: Full concurrency support with async/await and Sendable
  • Type-safe Encoding/Decoding: FirestoreEncoder and FirestoreDecoder for seamless Swift type conversion
  • Property Wrappers: @DocumentID, @ReferencePath, @ExplicitNull for Firestore-specific behaviors
  • Retry Strategy: Built-in retry handling with exponential backoff

Requirements

  • Swift 6.2+
  • macOS 15.0+ / iOS 18.0+ / watchOS 11.0+ / tvOS 18.0+ / visionOS 2.0+

Installation

Add FirebaseAPI to your Package.swift:

dependencies: [
    .package(url: "https://github.com/1amageek/FirebaseAPI.git", from: "0.1.0")
]

Then add it to your target dependencies:

.target(
    name: "YourTarget",
    dependencies: [
        .product(name: "FirestoreAPI", package: "FirebaseAPI")
    ]
)

Usage

Initialize Firestore

import FirestoreAPI
import GRPCHTTP2TransportNIOPosix // or your preferred transport

// Create a transport (example using HTTP/2 with NIO)
let transport = HTTP2ClientTransport.Posix(
    target: .ipv4(host: "firestore.googleapis.com", port: 443),
    config: .defaults(transportSecurity: .tls)
)

// Initialize Firestore with generic transport
let firestore = Firestore(
    projectId: "your-project-id",
    transport: transport,
    accessTokenProvider: yourAccessTokenProvider
)

Basic CRUD Operations

// Define your model
struct User: Codable {
    @DocumentID var id: String
    var name: String
    var email: String
    var createdAt: Timestamp
}

// Create a document
let userRef = firestore.collection("users").document("user123")
try await userRef.setData([
    "name": "John Doe",
    "email": "john@example.com",
    "createdAt": Timestamp.now()
], firestore: firestore)

// Or use Codable
let user = User(id: "user123", name: "John Doe", email: "john@example.com", createdAt: .now())
try await userRef.setData(user, firestore: firestore)

// Read a document
let snapshot = try await userRef.getDocument(firestore: firestore)
if let data = snapshot.data() {
    print("User data: \(data)")
}

// Or decode to Codable
let user: User? = try await userRef.getDocument(type: User.self, firestore: firestore)

// Update a document
try await userRef.updateData(["name": "Jane Doe"], firestore: firestore)

// Delete a document
try await userRef.delete(firestore: firestore)

Queries

// Simple query
let usersRef = firestore.collection("users")
let snapshot = try await usersRef
    .where("age" >= 18)
    .where("city" == "Tokyo")
    .orderBy("name", descending: false)
    .limit(10)
    .getDocuments(firestore: firestore)

for doc in snapshot.documents {
    print(doc.data())
}

// Query with Codable
let users: [User] = try await usersRef
    .where("age" >= 18)
    .getDocuments(type: User.self, firestore: firestore)

Transactions

try await firestore.runTransaction { transaction in
    // Read documents
    let userDoc = firestore.document("users/user123")
    let snapshot = try await transaction.get(documentReference: userDoc)

    guard let balance = snapshot.data()?["balance"] as? Int else {
        throw FirestoreError.notFound
    }

    // Write operations
    transaction.updateData(["balance": balance - 100], forDocument: userDoc)

    return balance - 100
}

Batch Writes

let batch = firestore.batch()

let user1 = firestore.document("users/user1")
let user2 = firestore.document("users/user2")

batch.setData(["name": "Alice"], forDocument: user1)
batch.updateData(["lastLogin": Timestamp.now()], forDocument: user2)
batch.deleteDocument(document: firestore.document("users/user3"))

try await batch.commit()

Property Wrappers

struct Post: Codable {
    @DocumentID var id: String
    @ReferencePath var path: String
    @ExplicitNull var deletedAt: Date?

    var title: String
    var content: String
    var authorRef: DocumentReference
}

// @DocumentID: Automatically populated with document ID during decoding
// @ReferencePath: Automatically populated with document path
// @ExplicitNull: Encodes as NSNull instead of omitting the field

Architecture

Generic Transport Design

FirebaseAPI uses a generic Transport parameter that conforms to ClientTransport from grpc-swift-2. This design allows:

  • Flexibility: Use any transport implementation (HTTP/2, NIO-based, custom)
  • Type Safety: Transport type is known at compile time for optimal performance
  • Testability: Easy to mock transport for unit tests
public final class Firestore<Transport: ClientTransport>: Sendable {
    internal let transport: Transport
    // ...
}

Why Generic Instead of Protocol?

The library uses Firestore<Transport: ClientTransport> instead of any ClientTransport because:

  1. gRPC Client Requirements: GRPCClient<Transport> requires a concrete type parameter
  2. Swift Type System: Existential types (any Protocol) cannot conform to protocols with Self requirements
  3. Performance: Generic types are resolved at compile time, avoiding runtime overhead

Development

Prerequisites

To develop this library, you need:

  1. Swift 6.2+
  2. Protocol Buffer compiler (protoc)
  3. gRPC Swift plugins

Generating Proto Files

This repository includes the googleapis as a submodule. To regenerate the Firestore proto files:

mkdir -p Sources/FirestoreAPI/Proto
cd googleapis
protoc \
  ./google/firestore/v1/*.proto \
  ./google/api/field_behavior.proto \
  ./google/api/resource.proto \
  ./google/longrunning/operations.proto \
  ./google/rpc/status.proto \
  ./google/type/latlng.proto \
  --swift_out=../Sources/FirestoreAPI/Proto \
  --grpc-swift_out=../Sources/FirestoreAPI/Proto \
  --swift_opt=Visibility=Public \
  --grpc-swift_opt=Visibility=Public

Running Tests

The test suite uses Swift Testing framework (not XCTest):

swift test

All 67 tests should pass:

  • Reference Path Tests: 9 tests
  • Query Predicate Tests: 6 tests
  • Firestore Encoder Tests: 21 tests
  • Firestore Decoder Tests: 23 tests
  • Listen API Tests: 8 tests

Test Coverage

  • ✅ Firestore Encoder/Decoder for all supported types
  • ✅ Document reference path generation
  • ✅ Query predicates and operators
  • ✅ Property wrappers (@DocumentID, @ReferencePath, @ExplicitNull)
  • ✅ Real-time listener response processing
  • ✅ Mock transport for testing without network calls

Dependencies

Migration from grpc-swift 1.x

This library has been migrated to grpc-swift-2.x. Key changes:

  • HPACKHeadersMetadata
  • ClientCallClientRequest with new API
  • Direct GRPCClient creation instead of connection pooling
  • Bidirectional streaming now fully supported for real-time listeners

Real-time Listeners

The library now supports real-time listeners for documents and queries using bidirectional streaming:

// Listen to document changes
let docRef = firestore.collection("users").document("user123")
let stream = try await docRef.addSnapshotListener(firestore: firestore)

for try await snapshot in stream {
    if snapshot.exists {
        print("Document updated: \(snapshot.data())")
    } else {
        print("Document deleted or doesn't exist")
    }
}
// Listen to query changes
let query = firestore.collection("users").where("age" >= 18)
let stream = try await query.addSnapshotListener(firestore: firestore)

for try await snapshot in stream {
    print("Query results updated: \(snapshot.documents.count) documents")
    for doc in snapshot.documents {
        print(doc.data())
    }
}

Note: The stream will continue until cancelled or an error occurs. Use task cancellation to stop listening:

let task = Task {
    for try await snapshot in stream {
        // Process snapshot
    }
}

// Later: stop listening
task.cancel()

License

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

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

Lightweight Cloud Firestore Client API using googleapis gRPC.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages