UNPKG

simplyfire

Version:

A lightweight firestore api for firebase cloud functions & Angular.

272 lines (264 loc) 9.43 kB
class AbstractFirestoreApi { constructor() { // Maximum number of writes that can be passed to a Commit operation // or performed in a transaction // https://cloud.google.com/firestore/quotas#writes_and_transactions this.BATCH_MAX_WRITES = 500; } getValueFromSnapshot(snapshot) { return (snapshot.exists ? snapshot.data() : null); } } class QueryBuilder { constructor() { this._where = []; this._orderBy = []; this._leftJoins = []; } get joins() { return this._leftJoins; } where(...where) { this._where.push(where); return this; } orderBy(...orderBy) { this._orderBy.push(orderBy); return this; } leftJoin(...leftJoin) { this._leftJoins.push(leftJoin); } limit(limit) { this._limit = limit; return this; } limitToLast(limitToLast) { this._limitToLast = limitToLast; return this; } startAt(...startAt) { this._startAt = startAt; return this; } startAfter(...startAfter) { this._startAfter = startAfter; return this; } endAt(...endAt) { this._endAt = endAt; return this; } endBefore(...endBefore) { this._endBefore = endBefore; return this; } // Still have to use <any> type due to most interfaces of @google-cloud/firestore // are not compatible with @firebase/firestore's interfaces. exec(ref, queryOps) { if (typeof window === 'undefined') { return this.execQueryForCloud(ref); } if (!queryOps) { throw Error('invalid arguments'); } const { query, where, orderBy, limit, limitToLast, startAt, startAfter, endAt, endBefore } = queryOps; const queryConstraints = [ ...this._where.map((w) => where(...w)), ...this._orderBy.map((o) => orderBy(...o)), ...(this._limit ? [limit(this._limit)] : []), ...(this._limitToLast ? [limitToLast(this._limitToLast)] : []), ...(this._startAt?.every((i) => !!i) ? [startAt(...this._startAt)] : []), ...(this._startAfter?.every((i) => !!i) ? [startAfter(...this._startAfter)] : []), ...(this._endAt?.every((i) => !!i) ? [endAt(...this._endAt)] : []), ...(this._endBefore?.every((i) => !!i) ? [endBefore(...this._endBefore)] : []) ]; return query(ref, ...queryConstraints); } execQueryForCloud(ref) { let query = this._where.reduce((q, wh) => q.where(...wh), ref); query = this._orderBy.reduce((q, ob) => q.orderBy(...ob), query); if (this._limit) { query = query.limit(this._limit); } if (this._limitToLast) { query = query.limitToLast(this._limitToLast); } if (this._startAt) { query = query.startAt(this._startAt); } if (this._startAfter) { query = query.startAfter(this._startAfter); } if (this._endAt) { query = query.endAt(this._endAt); } if (this._endBefore) { query = query.endBefore(this._endBefore); } return query; } } // chunk array to a certain size const arrayToChunks = (list, size) => { list = [...list]; return [...Array(Math.ceil(list.length / size))].map((_) => list.splice(0, size)); }; class FirestoreCloudService extends AbstractFirestoreApi { static getInstance(admin, settings = {}) { this.instance ?? (this.instance = new this()); this.instance.initialize(admin, settings); return this.instance; } initialize(admin, settings) { admin.initializeApp(); this.db = admin.firestore(); this.db.settings(settings); this.admin = admin; } // ----------------------------------------------------------------------------------------------------- // @ Abstract members // ----------------------------------------------------------------------------------------------------- async collection(collection, qb) { return (await this.collectionSnapshot(collection, qb)).docs.map((doc) => ({ id: doc.id, ...doc.data() })); } async collectionGroup(collectionId, qb) { return (await this.collectionGroupSnapshot(collectionId, qb)).docs.map((doc) => ({ id: doc.id, ...doc.data() })); } async doc(path) { const snapshot = await this.docRef(path).get(); return (snapshot.exists && { id: snapshot.id, ...snapshot.data() }) || null; } async upsert(collection, data, opts = { merge: true }) { const timestamp = this.serverTimestamp; // eslint-disable-next-line prefer-const let { id, ...updata } = data; updata.createdTs ?? (updata.createdTs = timestamp); if (!id) { id = this.db.collection(collection).doc().id; } updata.updatedTs = timestamp; await this.docRef(`${collection}/${id}`).set(Object.assign({}, updata), opts); return id; } async update(path, data) { await this.docRef(path).update(data); } async delete(path) { await this.docRef(path).delete(); } /** * Bulk update data */ async bulkUpsert(path, data, opts = { merge: true }) { const bulkIds = []; const promises = []; const timestamp = this.serverTimestamp; if (Array.isArray(data)) { // Due to a batch limitation, need to split docs array into chunks for (const chunks of arrayToChunks(data, this.BATCH_MAX_WRITES)) { const batch = this.batch; chunks.forEach((d) => { let { id, ...updata } = d; id ?? (id = this.db.collection(path).doc().id); updata.createdTs ?? (updata.createdTs = timestamp); updata.updatedTs = timestamp; batch.set(this.docRef(`${path}/${id}`), updata, opts); bulkIds.push(id); }); const p = batch.commit(); promises.push(p); } } else { const snapshot = await this.collectionSnapshot(path, data.qb); // Due to a batch limitation, need to split docs array into chunks for (const chunks of arrayToChunks(snapshot.docs, this.BATCH_MAX_WRITES)) { const batch = this.batch; chunks.forEach((d) => batch.set(d.ref, { updatedTs: timestamp, ...data.data }, opts) && bulkIds.push(d.id)); const p = batch.commit(); promises.push(p); } } await Promise.all(promises); return bulkIds; } /** * Bulk delete data */ async bulkDelete(collection, qb, maxSize = 1000) { if (!qb) { qb = new QueryBuilder(); qb.limit(maxSize); } const bulkIds = []; const promises = []; const snapshot = await this.collectionSnapshot(collection, qb); // Due to a batch limitation, need to split docs array into chunks for (const chunks of arrayToChunks(snapshot.docs, this.BATCH_MAX_WRITES)) { const batch = this.batch; chunks.forEach((doc) => batch.delete(doc.ref) && bulkIds.push(doc.id)); const p = batch.commit(); promises.push(p); } await Promise.all(promises); return bulkIds; } get batch() { return this.db.batch(); } get serverTimestamp() { return this.admin.firestore.FieldValue.serverTimestamp(); } increment(n = 1) { return this.admin.firestore.FieldValue.increment(n); } /** * Returns a generated Firestore Document Id. */ createId(colPath) { return this.db.collection(colPath ?? '_').doc().id; } runTransaction(updateFunction) { return this.db.runTransaction(updateFunction); } // Recursively delete a reference and log the references of failures. // https://github.com/googleapis/nodejs-firestore/pull/1494 recursiveDelete(ref, bulkWriter) { return this.db.recursiveDelete(ref, bulkWriter); } // ----------------------------------------------------------------------------------------------------- // @ Custom methods // ----------------------------------------------------------------------------------------------------- /** * Create a Firestore Timestamp * * @param date */ createTimestamp(date = new Date()) { return this.admin.firestore.Timestamp.fromDate(date); } collectionSnapshot(path, qb) { const collectionRef = this.db.collection(path); return (qb ? qb.exec(collectionRef) : collectionRef).get(); } collectionGroupSnapshot(collectionId, qb) { const groupRef = this.db.collectionGroup(collectionId); return (qb ? qb.exec(groupRef) : groupRef).get(); } docRef(path) { return this.db.doc(path); } } FirestoreCloudService.instance = null; /* * Public API Surface of common */ /* * Public API Surface of common */ /** * Generated bundle index. Do not edit. */ export { AbstractFirestoreApi, FirestoreCloudService, QueryBuilder, arrayToChunks }; //# sourceMappingURL=simplyfire.mjs.map