shelving
Version:
Toolkit for using data in JavaScript.
142 lines (141 loc) • 5.62 kB
JavaScript
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));
}
}