shelving
Version:
Toolkit for using data in JavaScript.
132 lines (131 loc) • 4.86 kB
JavaScript
import { addDoc, arrayRemove, arrayUnion, collection, deleteDoc, doc, documentId, getCount, getDoc, getDocs, increment, limit, orderBy, query, setDoc, updateDoc, where, } from "firebase/firestore/lite";
import { DBProvider } from "../../db/provider/DBProvider.js";
import { UnimplementedError } from "../../error/UnimplementedError.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 Lite client database provider.
* - Works with the Firebase JS SDK.
* - Does not support offline mode.
* - Does not support realtime subscriptions.
*/
export class FirestoreLiteProvider 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) {
throw new UnimplementedError("FirestoreLiteProvider does not support realtime subscriptions");
}
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 getCount(this._query(c, q));
return snapshot.data().count;
}
async getQuery(c, q) {
return _getItems(await getDocs(this._query(c, q)));
}
getQuerySequence(_c, _q) {
throw new UnimplementedError("FirestoreLiteProvider does not support realtime subscriptions");
}
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)));
}
}