UNPKG

simplyfire

Version:

A lightweight firestore api for firebase cloud functions & Angular.

440 lines (430 loc) 18.1 kB
import * as i0 from '@angular/core'; import { Injectable } from '@angular/core'; import 'reflect-metadata'; import { __awaiter, __rest } from 'tslib'; import { query, where, orderBy, limit, limitToLast, startAt, startAfter, endAt, endBefore, getFirestore, doc, setDoc, addDoc, collection, updateDoc, deleteDoc, writeBatch, serverTimestamp, increment, runTransaction, collectionData, collectionGroup, getDocs, collectionChanges, docData, documentId } from '@angular/fire/firestore'; import { lastValueFrom, of, defer, combineLatest } from 'rxjs'; import { map, take, switchMap } from 'rxjs/operators'; class ApplicationContext { static getInstance(options) { var _a; (_a = this.instance) !== null && _a !== void 0 ? _a : (this.instance = new this()); this.instance.initialize(options); } initialize(options) { this.options = options; } } ApplicationContext.injector = null; ApplicationContext.instance = null; ApplicationContext.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.0", ngImport: i0, type: ApplicationContext, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); ApplicationContext.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.0", ngImport: i0, type: ApplicationContext, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.0", ngImport: i0, type: ApplicationContext, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); const Autowire = () => { return (target, propertyKey) => { const provider = Reflect.getMetadata('design:type', target, propertyKey); let value; return { get: () => { return (value !== null && value !== void 0 ? value : (value = ApplicationContext.injector.get(provider))); }, set: (v) => { value = v; } }; }; }; 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) { var _a, _b, _c, _d; 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)] : []), ...(((_a = this._startAt) === null || _a === void 0 ? void 0 : _a.every((i) => !!i)) ? [startAt(...this._startAt)] : []), ...(((_b = this._startAfter) === null || _b === void 0 ? void 0 : _b.every((i) => !!i)) ? [startAfter(...this._startAfter)] : []), ...(((_c = this._endAt) === null || _c === void 0 ? void 0 : _c.every((i) => !!i)) ? [endAt(...this._endAt)] : []), ...(((_d = this._endBefore) === null || _d === void 0 ? void 0 : _d.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)); }; // flatten the array of observables to one level deep const flatten = (source) => { return source.pipe(map((arr) => arr.reduce((acc, cur) => acc.concat(cur)))); }; const queryOps = { query, where, orderBy, limit, limitToLast, startAt, startAfter, endAt, endBefore }; const CACHE_MAX_AGE = 5 * 60 * 1000; class FirestoreService extends AbstractFirestoreApi { constructor() { super(...arguments); this.cache = new Map(); } get firestore() { return getFirestore(); } // ----------------------------------------------------------------------------------------------------- // @ Abstract members // ----------------------------------------------------------------------------------------------------- collection(path, qb, maxAge = CACHE_MAX_AGE) { return lastValueFrom(this.collectionWithCache(path, qb, maxAge).pipe(take(1))); } collectionGroup(collectionId, qb, maxAge = CACHE_MAX_AGE) { return lastValueFrom(this.collectionGroupWithCache(collectionId, qb, maxAge).pipe(take(1))); } doc(path, maxAge = CACHE_MAX_AGE) { return lastValueFrom(this.docWithCache(path, maxAge).pipe(take(1))); } docRef(path) { return doc(this.firestore, path); } upsert(collectionPath, data, opts = { merge: true }) { var _a; return __awaiter(this, void 0, void 0, function* () { const timestamp = this.serverTimestamp; let { id } = data, updata = __rest(data, ["id"]); (_a = updata.createdTs) !== null && _a !== void 0 ? _a : (updata.createdTs = timestamp); updata.updatedTs = timestamp; if (id) { yield setDoc(this.docRef(`${collectionPath}/${id}`), Object.assign({}, updata), opts); } else { id = (yield addDoc(collection(this.firestore, collectionPath), updata)).id; } return id; }); } update(docPath, data) { const updatedTs = this.serverTimestamp; // ignore id delete data['id']; return updateDoc(this.docRef(docPath), Object.assign({}, data, { updatedTs })); } delete(docPath) { return deleteDoc(this.docRef(docPath)); } bulkUpsert(path, data, opts = { merge: true }) { return __awaiter(this, void 0, void 0, function* () { const bulkIds = []; const promises = []; 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; const timestamp = this.serverTimestamp; chunks.forEach((_a) => { var _b; return __awaiter(this, void 0, void 0, function* () { var { id } = _a, updata = __rest(_a, ["id"]); (_b = updata.createdTs) !== null && _b !== void 0 ? _b : (updata.createdTs = timestamp); updata.updatedTs = timestamp; let docRef; if (id) { docRef = this.docRef(`${path}/${id}`); } else { docRef = doc(collection(this.firestore, path)); } bulkIds.push(docRef.id); batch.set(docRef, updata, opts); }); }); const p = batch.commit(); promises.push(p); } } else { const snapshot = yield 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; const timestamp = this.serverTimestamp; chunks.forEach((d) => batch.set(d.ref, Object.assign({ updatedTs: timestamp }, data.data), opts) && bulkIds.push(d.id)); const p = batch.commit(); promises.push(p); } } yield Promise.all(promises); return bulkIds; }); } bulkDelete(path, qb, maxSize = 1000) { return __awaiter(this, void 0, void 0, function* () { qb !== null && qb !== void 0 ? qb : (qb = new QueryBuilder()); qb.limit(maxSize); const bulkIds = []; const promises = []; const snapshot = yield this.collectionSnapshot(path, 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.delete(d.ref) && bulkIds.push(d.id)); const p = batch.commit(); promises.push(p); } yield Promise.all(promises); return bulkIds; }); } /** * write batch */ get batch() { return writeBatch(this.firestore); } /** * firestore timestamp */ get serverTimestamp() { return serverTimestamp(); } /** * FieldValue increment */ increment(n = 1) { return increment(n); } /** * Returns a generated Firestore Document Id. */ createId(colPath) { return doc(collection(this.firestore, colPath !== null && colPath !== void 0 ? colPath : '_')).id; } runTransaction(updateFunction) { return runTransaction(this.firestore, updateFunction); } // ----------------------------------------------------------------------------------------------------- // @ Custom methods // ----------------------------------------------------------------------------------------------------- collectionValueChanges(path, qb) { const collectionRef = collection(this.firestore, path); qb !== null && qb !== void 0 ? qb : (qb = new QueryBuilder()); return collectionData(qb.exec(collectionRef, queryOps), { idField: 'id' }).pipe((s) => { var _a; return ((_a = qb === null || qb === void 0 ? void 0 : qb.joins) !== null && _a !== void 0 ? _a : []).map((j) => leftJoin(this, ...j)).reduce((ss, o) => o(ss), s); }); } collectionGroupValueChanges(collectionId, qb) { const collectionRef = collectionGroup(this.firestore, collectionId); qb !== null && qb !== void 0 ? qb : (qb = new QueryBuilder()); return collectionData(qb.exec(collectionRef, queryOps), { idField: 'id' }).pipe((s) => { var _a; return ((_a = qb === null || qb === void 0 ? void 0 : qb.joins) !== null && _a !== void 0 ? _a : []).map((j) => leftJoin(this, ...j)).reduce((ss, o) => o(ss), s); }); } collectionSnapshot(path, qb) { qb !== null && qb !== void 0 ? qb : (qb = new QueryBuilder()); const collectionRef = collection(this.firestore, path); return getDocs(qb.exec(collectionRef, queryOps)); } collectionSnapshotChanges(path, qb, events) { const collectionRef = collection(this.firestore, path); qb !== null && qb !== void 0 ? qb : (qb = new QueryBuilder()); return collectionChanges(qb.exec(collectionRef, queryOps), { events }).pipe(map((changes) => changes.map((c) => Object.assign({}, c.doc.data(), { id: c.doc.id }))), (s) => { var _a; return ((_a = qb === null || qb === void 0 ? void 0 : qb.joins) !== null && _a !== void 0 ? _a : []).map((j) => leftJoin(this, ...j)).reduce((ss, o) => o(ss), s); }); } collectionGroupSnapshotChanges(collectionId, qb, events) { const collectionRef = collectionGroup(this.firestore, collectionId); qb !== null && qb !== void 0 ? qb : (qb = new QueryBuilder()); return collectionChanges(qb.exec(collectionRef, queryOps), { events }).pipe(map((changes) => changes.map((c) => Object.assign({}, c.doc.data(), { id: c.doc.id }))), (s) => { var _a; return ((_a = qb === null || qb === void 0 ? void 0 : qb.joins) !== null && _a !== void 0 ? _a : []).map((j) => leftJoin(this, ...j)).reduce((ss, o) => o(ss), s); }); } docValueChanges(path) { const docRef = doc(this.firestore, path); return docData(docRef, { idField: 'id' }); } /** * @experimental * * Cache collection data in memory */ collectionWithCache(path, qb, maxAge) { return this.fetchFromCache(path + (qb ? JSON.stringify(qb) : ''), this.collectionValueChanges(path, qb), maxAge); } /** * @experimental * * Cache collectionGroup data in memory */ collectionGroupWithCache(collectionId, qb, maxAge) { return this.fetchFromCache(collectionId + (qb ? JSON.stringify(qb) : ''), this.collectionGroupValueChanges(collectionId, qb), maxAge); } /** * @experimental * * Cache document data in memory */ docWithCache(path, maxAge) { return this.fetchFromCache(path, this.docValueChanges(path), maxAge); } /** * @experimental * * Delete cached data from the memory */ deleteCache(path, qb) { const key = path + (qb ? JSON.stringify(qb) : ''); return this.cache.delete(key); } fetchFromCache(key, source, maxAge) { const cached = this.cache.get(key); if (maxAge === 0 || !cached || (maxAge && Date.now() - cached.lastRead > maxAge)) { return source.pipe(map((data) => this.cache.set(key, { lastRead: Date.now(), data }) && data)); } return of(cached.data); } } FirestoreService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.0", ngImport: i0, type: FirestoreService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); FirestoreService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.0", ngImport: i0, type: FirestoreService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.0", ngImport: i0, type: FirestoreService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); const leftJoin = (fs, key, collection, alias, maxAge) => { if (key === alias) { throw Error('Due to use of Cache, you must use different alias for a key.'); } return (source) => defer(() => { let ret; return source.pipe(switchMap((data) => { ret = data; const fetchJoinData = (id) => { if (Array.isArray(id)) { const qb = new QueryBuilder(); qb.where(documentId(), 'in', id); return fs.collectionWithCache(collection, qb, maxAge); } return fs.docWithCache(`${collection}/${id}`, maxAge); }; if (Array.isArray(data)) { const docs$ = ret.filter((i) => i[key]).map((i) => fetchJoinData(i[key])); return docs$.length ? combineLatest(docs$) : of([]); } return data && data[key] ? fetchJoinData(data[key]) : of(null); }), map((joins) => { if (Array.isArray(ret)) { return ret.map((r) => { const id = r[key]; if (id) { if (Array.isArray(id)) { r[alias] = joins .filter((j) => Array.isArray(j) && JSON.stringify(j.map((jj) => jj.id).sort()) === JSON.stringify(id.sort())) .pop(); } else { r[alias] = joins.filter((j) => (j === null || j === void 0 ? void 0 : j.id) === id).pop(); } } return r; }); } if (ret) { ret[alias] = joins; } return ret; })); }); }; /** * Generated bundle index. Do not edit. */ export { ApplicationContext, Autowire, FirestoreService, arrayToChunks, flatten }; //# sourceMappingURL=simplyfire-ngx.mjs.map