npm package: @megasquid/firestore-fetch
Description: A powerful, observable-based Firestore data fetcher for web apps using the official Firebase JavaScript SDK.
Author: Adam Koncz
Latest version: 0.1.5

# @megasquid/firestore-fetch

A powerful, observable-based Firestore data fetcher for web apps using the official Firebase JavaScript SDK.

`@megasquid/firestore-fetch` provides a reactive and highly flexible way to query and compose data from Firestore. It focuses on reads: streaming documents and collections, composing multi-source fetches (including subcollections and reference resolution), and automatically injecting lightweight `_meta` information into every document.

Key differentiators:
- Observable-first API built on RxJS (streams for `doc$`, `col$`, `colGroup$`, and the advanced `fetch()` builder).
- Declarative `FetchConfig` to describe complex data shapes (multiple docs, collections, subcollections, and properties) in one call.
- Automatic `_meta` augmentation (path, id, segments, timestamps) so downstream code always has identity and path info.
- Designed for the standard `firebase/firestore` JavaScript SDK (modular or namespaced use).

Installation
```
npm install @megasquid/firestore-fetch firebase
```

Basic usage

The package is read-focused. It’s intended to be extended by your app to add write helpers (so you can adapt write semantics and security rules as needed). Example of creating an Angular service that extends `FirestoreFetch`:

```ts
// app/services/firestore.service.ts (Angular example)
import { Injectable } from '@angular/core';
import { FirestoreFetch, Meta } from '@megasquid/firestore-fetch';

@Injectable({ providedIn: 'root' })
export class FirestoreService extends FirestoreFetch {
	constructor() {
		// Provide a meta factory so `_meta` is created for new docs
		super((path: string) => ({
			path,
			id: path.split('/').pop() || '',
			segments: path.split('/'),
			createdAt: new Date().toISOString(),
			updatedAt: new Date().toISOString(),
		}));
	}

	// Add write methods (see "Extending with Write Operations")
}
```

Core concepts

- Automatic `_meta` field: every document returned from `doc$`, `col$`, `colGroup$` or `fetch()` is enriched with `_meta` containing `path`, `id`, `segments` and optional timestamps. This makes it trivial to persist changes or resolve related resources.
- Lightweight DocumentReference: use `ref(path)` to create a serializable `{ id, path }` reference (preferred for storing refs in documents that will be passed through JSON or stored in Firestore fields).
- Declarative fetch: `fetch()` accepts a `FetchConfig | FetchConfig[]` describing what to fetch. Use `asNamedProperty` to shape results and `subCollections`/`properties` to attach nested data.

API overview

- doc$<T>(path: string): Observable<(T & Meta) | null> — Stream a single document with `_meta`.
- 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> — Collection group queries across all parents.
- fetch<T>(config|configs, buildConfig?): Observable<T> — Advanced multi-source builder that composes results, resolves references, and attaches subcollections.
- id(): string — Generate a Firestore-compatible random document id.
- ref(path: string) — Create a lightweight `{ id, path }` reference object.
- clone(obj: any): any — Deep clone preserving Firestore-like values (refs, timestamps where applicable).

Practical recipes

- Listen to a document:

```ts
this.ffs.doc$<User>('users/abc').subscribe(user => {
	// user._meta.path === 'users/abc'
});
```

- Stream a collection with filters and ordering:

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

- Fetch a document with subcollections and resolved refs:

```ts
this.ffs.fetch<{ hunt: Hunt & Meta }>([
	{
		path: 'hunts/abc',
		asNamedProperty: 'hunt',
		subCollections: [ { path: 'loot' }, { path: 'rewards' } ],
		properties: { '$.coverImage': {} }
	}
], { merge: true, onlyNamedProperties: true })
.subscribe(({ hunt }) => {
	console.log(hunt.loot, hunt.coverImage);
});
```

Extending with Write Operations

`@megasquid/firestore-fetch` purposely focuses on reads so you can implement writes in the way that fits your app and security rules. Example using the official Firebase SDK's write helpers:

```ts
import { getFirestore, doc, setDoc, updateDoc, deleteDoc } from 'firebase/firestore';

class FirestoreService extends FirestoreFetch {
	private db = getFirestore();

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

	async update(path: string, patch: any) {
		const patchWithMeta = { ...patch, '_meta.updatedAt': new Date().toISOString() } as any;
		await updateDoc(doc(this.db, path), patchWithMeta);
	}

	async delete(path: string) {
		await deleteDoc(doc(this.db, path));
	}
}
```

Documentation and examples

For real-world usage examples (how the library is used across an app, pipes, services, and recipes) see the project docs:

- docs/firestore-fetch.md — practical examples and recipes from a real app (LootletApp).

Contributing and notes

- This package is intentionally read-centric and designed to be extended for writes so your app can keep full control of security, validation, and triggers.
- If you use server SDKs or cloud functions, adapt write helpers accordingly (use Admin SDK patterns server-side).

License

MIT

Last updated: 2025-10-26