UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

142 lines (141 loc) 5.62 kB
import { FieldPath, FieldValue, Firestore } from "@google-cloud/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 = FieldPath.documentId(); const BATCH_SIZE = 1000; // Map `Filter.types` to `WhereFilterOp` const OPERATORS = { is: "==", not: "!=", in: "in", out: "not-in", contains: "array-contains", gt: ">", gte: ">=", lt: "<", lte: "<=", }; 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 `Update` instances 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, FieldValue.increment(value)]; if (action === "with") return [k, FieldValue.arrayUnion(...value)]; if (action === "omit") return [k, FieldValue.arrayRemove(...value)]; return action; // Never happens. } /** * Firestore server database provider. * - Works with the Firebase Admin SDK for Node.JS */ export class FirestoreServerProvider extends DBProvider { _firestore; constructor(firestore = new Firestore()) { super(); this._firestore = firestore; } /** Create a corresponding `FirestoreCollection` reference from a collection. */ _getCollection(collection) { return this._firestore.collection(collection.name); } /** Create a corresponding `FirestoreQuery` reference from a collection and query. */ _getQuery(c, q) { let ref = this._getCollection(c); if (q) { for (const { key, direction } of getQueryOrders(q)) { const k = joinDataPath(key); ref = ref.orderBy(k === "id" ? ID : k, direction); } for (const { key, operator, value } of getQueryFilters(q)) { const k = joinDataPath(key); ref = ref.where(k === "id" ? ID : k, OPERATORS[operator], value); } const l = getQueryLimit(q); if (typeof l === "number") ref = ref.limit(l); } return ref; } /** Perform a bulk update on a set of documents using a `BulkWriter` */ async _bulkWrite(c, q, callback) { const writer = this._firestore.bulkWriter(); const ref = this._getQuery(c, q).limit(BATCH_SIZE).select(); // `select()` turns the query into a field mask query (with no field masks) which saves data transfer and memory. let current = ref; while (current) { const { docs, size } = await current.get(); for (const s of docs) callback(writer, s); current = size >= BATCH_SIZE && ref.startAfter(docs.pop()).select(); void writer.flush(); } await writer.close(); } async getItem(collection, id) { return _getOptionalItem(await this._getCollection(collection).doc(id).get()); } getItemSequence(c, id) { const ref = this._getCollection(c).doc(id); const sequence = new DeferredSequence(); return new LazySequence(sequence, () => ref.onSnapshot(snapshot => sequence.resolve(_getOptionalItem(snapshot)), reason => sequence.reject(reason))); } async addItem(c, data) { return (await this._getCollection(c).add(data)).id; // `as II` needed: Firestore returns string, not II. } async setItem(c, id, data) { await this._getCollection(c).doc(id).set(data); } async updateItem(c, id, updates) { await this._getCollection(c).doc(id).update(_getFieldValues(updates)); } async deleteItem(c, id) { await this._getCollection(c).doc(id).delete(); } async countQuery(c, q) { const snapshot = await this._getQuery(c, q).count().get(); return snapshot.data().count; } async getQuery(c, q) { return _getItems(await this._getQuery(c, q).get()); } getQuerySequence(c, q) { const ref = this._getQuery(c, q); const sequence = new DeferredSequence(); return new LazySequence(sequence, () => ref.onSnapshot(snapshot => sequence.resolve(_getItems(snapshot)), reason => sequence.reject(reason))); } async setQuery(c, q, data) { return await this._bulkWrite(c, q, (w, s) => void w.set(s.ref, data)); } async updateQuery(c, q, updates) { const fieldValues = _getFieldValues(updates); return await this._bulkWrite(c, q, (w, s) => void w.update(s.ref, fieldValues)); } async deleteQuery(c, q) { return await this._bulkWrite(c, q, (w, s) => void w.delete(s.ref)); } }