UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

135 lines (134 loc) 5.15 kB
import { addDoc, arrayRemove, arrayUnion, collection, deleteDoc, doc, documentId, getCountFromServer, getDoc, getDocs, increment, limit, onSnapshot, orderBy, query, setDoc, updateDoc, where, } from "firebase/firestore"; import { DBProvider } from "../../db/provider/DBProvider.js"; import { DeferredSequence } from "../../sequence/DeferredSequence.js"; import { LazySequence } from "../../sequence/LazySequence.js"; import { joinDataPath } from "../../util/data.js"; import { getItem } from "../../util/item.js"; import { getObject } from "../../util/object.js"; import { getQueryFilters, getQueryLimit, getQueryOrders } from "../../util/query.js"; import { mapItems } from "../../util/transform.js"; import { getUpdates } from "../../util/update.js"; // Constants. const ID = documentId(); // Map `Filter.types` to `WhereFilterOp` const OPERATORS = { is: "==", not: "!=", in: "in", out: "not-in", contains: "array-contains", gt: ">", gte: ">=", lt: "<", lte: "<=", }; function* _getConstraints(q) { for (const { key, direction } of getQueryOrders(q)) { const k = joinDataPath(key); yield orderBy(k === "id" ? ID : k, direction); } for (const { key, operator, value } of getQueryFilters(q)) { const k = joinDataPath(key); yield where(k === "id" ? ID : k, OPERATORS[operator], value); } const l = getQueryLimit(q); if (typeof l === "number") yield limit(l); } function _getItems(snapshot) { return snapshot.docs.map(s => _getItem(s)); } function _getItem(snapshot) { return getItem(snapshot.id, snapshot.data()); // `as II` needed: Firestore snapshot.id is always string, not II. } function _getOptionalItem(snapshot) { const data = snapshot.data(); if (data) return getItem(snapshot.id, data); // `as II` needed: Firestore snapshot.id is always string, not II. } /** Convert `Updates` object into corresponding Firestore `FieldValue` instances. */ function _getFieldValues(updates) { return getObject(mapItems(getUpdates(updates), _getFieldValue)); } function _getFieldValue({ key, action, value }) { const k = joinDataPath(key); if (action === "set") return [k, value]; if (action === "sum") return [k, increment(value)]; if (action === "with") return [k, arrayUnion(...value)]; if (action === "omit") return [k, arrayRemove(...value)]; return action; // Never happens. } /** * Firestore client database provider. * - Works with the Firebase JS SDK. * - Supports offline mode. * - Supports realtime subscriptions. */ export class FirestoreClientProvider extends DBProvider { _firestore; constructor(firestore) { super(); this._firestore = firestore; } /** Get a Firestore CollectionReference for a given collection. */ _collection({ name }) { return collection(this._firestore, name); } /** Get a Firestore DocumentReference for a given document. */ _doc(c, id) { return doc(this._collection(c), id); } /** Get a Firestore QueryReference for a given query. */ _query(c, q) { return q ? query(this._collection(c), ..._getConstraints(q)) : this._collection(c); } async getItem(c, id) { const snapshot = await getDoc(this._doc(c, id)); return _getOptionalItem(snapshot); } getItemSequence(c, id) { const sequence = new DeferredSequence(); return new LazySequence(sequence, () => onSnapshot(this._doc(c, id), snapshot => sequence.resolve(_getOptionalItem(snapshot)), reason => sequence.reject(reason))); } async addItem(c, data) { const reference = await addDoc(this._collection(c), data); return reference.id; // `as II` needed: Firestore returns string, not II. } async setItem(c, id, data) { await setDoc(this._doc(c, id), data); } async updateItem(c, id, updates) { await updateDoc(this._doc(c, id), _getFieldValues(updates)); } async deleteItem(c, id) { await deleteDoc(this._doc(c, id)); } async countQuery(c, q) { const snapshot = await getCountFromServer(this._query(c, q)); return snapshot.data().count; } async getQuery(c, q) { return _getItems(await getDocs(this._query(c, q))); } getQuerySequence(c, q) { const sequence = new DeferredSequence(); return new LazySequence(sequence, () => onSnapshot(this._query(c, q), snapshot => sequence.resolve(_getItems(snapshot)), reason => sequence.reject(reason))); } async setQuery(c, q, data) { const snapshot = await getDocs(this._query(c, q)); await Promise.all(snapshot.docs.map(s => setDoc(s.ref, data))); } async updateQuery(c, q, updates) { const snapshot = await getDocs(this._query(c, q)); const fieldValues = _getFieldValues(updates); await Promise.all(snapshot.docs.map(s => updateDoc(s.ref, fieldValues))); } async deleteQuery(c, q) { const snapshot = await getDocs(this._query(c, q)); await Promise.all(snapshot.docs.map(s => deleteDoc(s.ref))); } }