simplyfire
Version:
A lightweight firestore api for firebase cloud functions & Angular.
440 lines (430 loc) • 18.1 kB
JavaScript
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