npm package: @megasquid/firestore-fetch-cap
Description: A powerful, observable-based Firestore data fetcher for Capacitor apps.
Author: Adam Koncz
Latest version: 0.0.17

# @megasquid/firestore-fetch-cap

A powerful, observable-based Firestore data fetcher for Capacitor apps.

`@megasquid/firestore-fetch-cap` provides a reactive and highly flexible way to query and compose data from Firestore in an Ionic/Angular + Capacitor environment. It's designed to handle complex data requirements, including nested subcollections, document reference resolution, and multi-source data composition, all through a single, powerful `fetch` method.

It's built on top of `@capacitor-firebase/firestore` and uses `rxjs` to deliver real-time data streams.

## Features

- **Reactive API**: Uses `rxjs` Observables for real-time data streams (`doc$`, `col$`, `colGroup$`).
- **Advanced Fetching**: A powerful `fetch()` method to query documents, collections, subcollections, and resolve nested document references in a single operation.
- **Automatic Metadata**: Injects a `_meta` field into every document with its `path`, `id`, `segments`, and optional timestamps.
- **Capacitor-Friendly**: Works seamlessly with `@capacitor-firebase/firestore`, using lightweight, serializable document references (`{ id, path }`).
- **Lightweight & Focused**: Provides a robust read-only query engine. It is designed to be extended with your own write logic.
- **Complex Queries Made Simple**: Define your entire data shape with a declarative `FetchConfig` object, and let `firestore-fetch-cap` handle the complex orchestration of fetching, merging, and streaming the results.

## Installation

```bash
npm install @megasquid/firestore-fetch-cap rxjs @capacitor-firebase/firestore
```

## Basic Usage

Instantiate `FirestoreFetch` in your Angular service. You can provide a custom factory to generate `_meta` objects.

```typescript
// In your firestore.service.ts
import { Injectable } from '@angular/core';
import { FirestoreFetch, Meta } from '@megasquid/firestore-fetch-cap';

@Injectable({ providedIn: 'root' })
export class FirestoreService extends FirestoreFetch {
  constructor() {
    // Pass a custom meta factory to the parent constructor
    super((path: string) => {
      return {
        path: path,
        id: path.split('/').pop() || '',
        segments: path.split('/'),
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      };
    });
  }

  // Add your write methods here (set, update, delete)
  // by wrapping @capacitor-firebase/firestore calls.
}
```

## Core Concepts

### Automatic `_meta` Field

Every document retrieved by `firestore-fetch-cap` is automatically enriched with a `_meta` property. This provides crucial identity and path information directly on the data object, which is useful for subsequent write operations or for component logic.

```typescript
const user: User & Meta = {
  name: 'Alice',
  _meta: {
    path: 'users/123',
    id: '123',
    segments: ['users', '123'],
    createdAt: '2025-01-01T00:00:00.000Z',
    updatedAt: '2025-01-02T00:00:00.000Z'
  }
};
```

### Capacitor-Friendly Document References

Instead of using the native Firebase `DocumentReference` object, which is not serializable and can be problematic with Capacitor, `firestore-fetch-cap` provides a `ref()` utility to create lightweight reference objects.

```typescript
const imageRef = ffs.ref('system/data/media/abc123');
// Returns: { id: 'abc123', path: 'system/data/media/abc123' }

// You can then use this object in your write operations:
// await ffs.update('hunts/xyz', { coverImage: imageRef });
```

## API Overview

### Reading Data

- `doc$<T>(path: string): Observable<(T & Meta) | null>`: Stream a single document.
- `col$<T>(path: string, filters?, orderBy?, limit?): Observable<(T & Meta)[] | null>`: Stream a collection with optional filters, ordering, and limit.
- `colGroup$<T>(segment: string, filters?, orderBy?, limit?): Observable<(T & Meta)[] | null>`: Stream a collection group.

### The `fetch()` Method

The `fetch<T>()` method is the cornerstone of this library. It allows you to define all your data needs in a single configuration object and receive a composed, observable result. It can fetch multiple documents, collections, and subcollections, and even resolve nested document references automatically.

```typescript
fetch<T>(
  fetchConfigOrConfigs: FetchConfig | FetchConfig[],
  buildConfig?: { merge: boolean, onlyNamedProperties: boolean }
): Observable<T>
```

The `FetchConfig` object lets you define:
- `path`: A single document path, collection path, or an array of document paths.
- `asNamedProperty`: A name to use for this data in the output object.
- `filters`, `orderBy`, `limit`: For collection queries.
- `subCollections`: An array of `FetchConfig` objects for nested subcollections.
- `properties`: An object to resolve nested document references within a document.

### Utility Methods

- `id(): string`: Generates a random, Firestore-compatible document ID.
- `ref(path: string): DocumentReference`: Creates a lightweight, Capacitor-friendly document reference.
- `clone(obj: any): any`: Deep clones an object while preserving Firestore-specific types like `Timestamp` and `DocumentReference`.

## Practical Recipes

### Listen to a Single Document

```typescript
this.ffs.doc$<Hunt>('hunts/abc123').subscribe(hunt => {
  // react to real-time changes
});
```

### Stream a Collection with Filters

```typescript
import { OrderByDirection } from '@capacitor-firebase/firestore';

this.ffs.col$<Hunt>(
  'hunts',
  [ ['published','==',true] ],
  ['createdAt', 'desc' as OrderByDirection],
  20,
).subscribe(hunts => {
  // hunts: (Hunt & Meta)[]
});
```

### Fetch a Document with its Subcollections

Use `fetch()` to get a document and its related collections in a single, structured object.

```typescript
this.ffs.fetch<{ hunt: Hunt & Meta }>({
  path: 'hunts/abc123',
  asNamedProperty: 'hunt',
  subCollections: [
    { path: 'loot' }, // Attaches `loot` array to the hunt object
    { path: 'rewards' } // Attaches `rewards` array
  ]
}, { merge: true, onlyNamedProperties: true })
.subscribe(({ hunt }) => {
  console.log(hunt.loot);    // Array of loot documents
  console.log(hunt.rewards); // Array of reward documents
});
```

### Resolve Document References in a Document

If a document contains fields that are document references, `fetch()` can automatically resolve them.

```typescript
// Given a 'hunts/abc123' document:
// { title: 'My Hunt', coverImage: { id: 'img456', path: 'media/img456' } }

this.ffs.fetch<{ hunt: Hunt & Meta }>({
  path: 'hunts/abc123',
  asNamedProperty: 'hunt',
  properties: {
    '$.coverImage': {} // Auto-resolves the reference at this JSONPath
  }
}, { merge: true, onlyNamedProperties: true })
.subscribe(({ hunt }) => {
  console.log(hunt.coverImage.url); // The resolved media document is now here
});
```

### Fetch an Explicit List of Documents

Pass an array of paths to fetch multiple specific documents at once.

```typescript
const huntIds = ['abc123', 'def456'];
const paths = huntIds.map(id => `hunts/${id}`);

this.ffs.fetch<{ hunts: (Hunt & Meta)[] }>({
  path: paths,
  asNamedProperty: 'hunts',
}, { merge: true, onlyNamedProperties: true })
.subscribe(({ hunts }) => {
  // use array of hunt documents
});
```

### Fetch Multiple, Unrelated Sources

Combine different data sources into a single observable stream.

```typescript
this.ffs.fetch<{ user: User & Meta, settings: Settings & Meta }>([
  { path: 'users/123', asNamedProperty: 'user' },
  { path: 'users/123/settings', asNamedProperty: 'settings' }
], { merge: true, onlyNamedProperties: true })
.subscribe(({ user, settings }) => {
  console.log(user.name, settings.theme);
});
```

## Extending with Write Operations

`@megasquid/firestore-fetch-cap` is designed to be lean and focuses exclusively on advanced data fetching. You can easily extend the `FirestoreFetch` class to add write methods (`set`, `update`, `delete`) by wrapping the corresponding functions from `@capacitor-firebase/firestore`.

This approach keeps the core library focused while giving you full control over your application's write logic and `_meta` field updates.

```typescript
// In your extended FirestoreService
import { setDocument, updateDocument, deleteDocument } from '@capacitor-firebase/firestore';

// ...

async set(path: string, data: any, options = { merge: true }) {
  const meta = (data._meta)
    ? { ...data._meta, updatedAt: new Date().toISOString() }
    : this.createMeta(path);

  const dataWithMeta = { ...data, _meta: meta };
  await setDocument({ reference: path, data: dataWithMeta, merge: options.merge });
  return dataWithMeta;
}

async update(path: string, patch: any) {
  const dataWithMeta = { ...patch, '_meta.updatedAt': new Date().toISOString() };
  await updateDocument({ reference: path, data: dataWithMeta });
}

async delete(path: string) {
  await deleteDocument({ reference: path });
}
```