UNPKG

yalento

Version:

An awesome integration of Google Firebase for Angular and Node

375 lines (374 loc) 16.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FirestoreConnector = exports.Firebase = exports.Firestore = void 0; const guid_typescript_1 = require("guid-typescript"); const ngeohash_1 = require("ngeohash"); const rxjs_1 = require("rxjs"); const AbstractConnector_1 = require("./AbstractConnector"); //eslint-disable-next-line @typescript-eslint/no-var-requires const { Parser } = require('js-sql-parser'); class Firestore { } exports.Firestore = Firestore; class Firebase { } exports.Firebase = Firebase; class FirestoreConnector extends AbstractConnector_1.AbstractConnector { constructor(repository, db, options) { super(repository); this.dataMode = 'ALL'; this.realtimeMode = true; this.currentUser = { uid: null }; this.currentUser$ = new rxjs_1.BehaviorSubject({ uid: null }); this.lastSql = ''; this.observers = []; this.options = options ? options : {}; if (this.options.dataMode) { this.dataMode = this.options.dataMode; } if (this.options.realtimeMode === false) { this.realtimeMode = false; } this.debug = !!this.options.debug; /* istanbul ignore next */ this.db = db.firestore ? db.firestore : db; this.app = this.db.app; if (this.app && typeof this.app.auth === 'function') { this.app.auth().onAuthStateChanged((state) => { this.currentUser = { uid: state ? state.uid : null, }; this.currentUser$.next(this.currentUser); }); } else { this.currentUser$.next({ uid: 'ANONYMOUS_' + guid_typescript_1.Guid.create().toString() }); } } getUserUuid() { const o = new rxjs_1.Observable((observer) => { this.currentUser$.subscribe((u) => { if (u && u.uid !== undefined) { observer.next(u.uid ? u.uid : ''); } }); }); this.observers.push(o); return o; } isPrivateMode() { return this.dataMode === 'PRIVATE'; } add(items) { return new Promise((resolve, reject) => { try { items.forEach((item) => { if (!item['__timestamp'] || item['__timestamp'] > this.repository.getCreationTimestamp()) { const data = item._toPlain(); if (this.options.nearBy !== undefined) { data['__geohash'] = ngeohash_1.default.encode(this.options.nearBy.lat.getValue(), this.options.nearBy.long.getValue()); data['__latitude'] = this.options.nearBy.lat.getValue(); data['__longitude'] = this.options.nearBy.long.getValue(); data['__geopoint'] = new this.app.firebase_.firestore.GeoPoint(this.options.nearBy.lat.getValue(), this.options.nearBy.long.getValue()); } data['__uuid'] = item['__uuid']; data['__owner'] = item['__owner'] ? item['__owner'] : {}; if (this.currentUser.uid && this.currentUser.uid !== 'null') { data['__owner'][this.currentUser.uid] = true; } if (this.dataMode !== 'PRIVATE') { data['__owner']['EVERYBODY'] = true; } if (data['__references'] !== undefined) { delete data['__references']; } const docReference = this.db.doc(this.getPath() + '/' + item.__uuid); if (this.debug) { this.debugMessage(`firebase writes to ${this.getPath() + '/' + item.__uuid}`, data); } docReference .set(data, { merge: true }) .then(() => { if (this.options.parent) { const references = {}; references[this.repository.getClassName()] = { name: this.repository.getClassName(), lastUpdated: new Date(), }; this.db .doc(this.getParentDocumentPath()) .set({ __references: references }, { merge: true }) .then() .catch((e) => { reject('error while creating firestore document "' + this.getPath() + '/' + item.__uuid + '": ' + e.message); }); } }) .catch((e) => { if (this.debug) { this.debugMessage('error while creating firestore document "' + this.getPath() + '/' + item.__uuid + '": ' + e.message); } reject(); }); } else { if (this.debug) { this.debugMessage(`firebase skipped writing to ${this.getPath() + '/' + item.__uuid} while remote data is not synchronized.`); } } }); resolve(null); } catch (e) { reject(e); } }); } update(items) { return __awaiter(this, void 0, void 0, function* () { return this.add(items); }); } remove(items) { return __awaiter(this, void 0, void 0, function* () { this.lastSql = ''; const promises = []; try { items.forEach((item) => { const docReference = this.db.doc(this.getPath() + '/' + item.__uuid); promises.push(docReference.delete()); }); } catch (e) { return Promise.reject(e); } yield Promise.all(promises); }); } select(sql) { return __awaiter(this, void 0, void 0, function* () { let hasGeoLocations = false; const originalSqlParts = sql.split(' WHERE ', 2); let finalSql = this.replaceSql(originalSqlParts[1], !!(this.options.nearBy && this.options.nearBy.radius.getValue() > 0)); if (this.options.nearBy && this.options.nearBy.radius.getValue() > 0) { hasGeoLocations = true; const range = this.getGeohashRange(this.options.nearBy.lat.getValue(), this.options.nearBy.long.getValue(), this.options.nearBy.radius.getValue()); const geoquery = ' AND __geohash >= "' + range.lower + '" AND __geohash <= "' + range.upper + '" '; finalSql += geoquery; this.repository.setGeoQuery(geoquery + ' AND __distance <= ' + this.options.nearBy.radius.getValue()); } else { this.repository.setGeoQuery(); } if (this.lastSql !== finalSql) { if (this.debug) { this.debugMessage(`firebase subscribes to ${finalSql}`); } if (typeof this.firestoreCollectionSnapshotUnsubscribe === 'function') { this.firestoreCollectionSnapshotUnsubscribe(); } this.firestoreCollectionSnapshotUnsubscribe = this.getFirebaseCollection(finalSql).onSnapshot((querySnapshot) => { const repository = this.repository; querySnapshot.docChanges().forEach((change) => { if (change.type === 'removed') { repository .remove({ __uuid: change.doc.id }, 'firestore') .then() .catch(); } }); this.repository .createMany(querySnapshot.docs.map((value) => value.exists ? value.data() : Object.assign(Object.assign({}, value.data()), { __removed: true })), '', 'firestore') .then(); if (hasGeoLocations && this.options.nearBy) { this.repository.updateGeoLocations(querySnapshot.docs.map((value) => value.data()), this.options.nearBy.lat.getValue(), this.options.nearBy.long.getValue()); } }); this.lastSql = finalSql; } }); } disconnect() { return __awaiter(this, void 0, void 0, function* () { if (this.firestoreCollectionSnapshotUnsubscribe) { this.firestoreCollectionSnapshotUnsubscribe(); } if (this.observers.length) { this.observers.forEach((o) => { if (typeof o.unsubscribe === 'function') { o.unsubscribe(); } }); } if (this.debug) { this.debugMessage(`firebase disconnected`); } }); } selectOneByIdentifier(identifier) { return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { if (this.debug) { this.debugMessage(`firebase read once ${this.getPath() + '/' + identifier}`); } this.db .doc(this.getPath() + '/' + identifier) .get() .then((data) => { if (!data.exists) { resolve(null); } else { this.repository .create(data.data(), identifier, undefined, 'firestore') .then((e) => { resolve(e); }) .catch(() => { resolve(null); }); } }); })); } /** * * @param sql * @param hasGeoCondition */ replaceSql(sql, hasGeoCondition) { let statement = sql; // statement = statement.replace(new RegExp(/'(.*)' IN \(([^)]*)\)/, 'gm'), '`$2.$1`'); statement = statement.replace(new RegExp(/`/, 'gm'), ''); const inequalityMatch = statement.match(/( < | <= | > | >= | != )/g); if (inequalityMatch && inequalityMatch.length >= (hasGeoCondition ? 0 : 2)) { let replacedStatement = ''; statement.split(/( AND | OR )/g).forEach((part) => { if (!part.match(/( < | <= | > | >= | != )/g)) { replacedStatement += part; } else { replacedStatement += '1==1'; } }); statement = replacedStatement.replace(/OR 1==1/g, '').replace(/AND 1==1/g, ''); } return statement; } debugMessage(message, data) { // tslint:disable-next-line:no-console console.log(message, data ? data : ''); } getGeohashRange(latitude, longitude, distance) { const lat = 0.0144927536231884; // degrees latitude per mile const lon = 0.0181818181818182; // degrees longitude per mile const lowerLat = latitude - lat * distance; const lowerLon = longitude - lon * distance; const upperLat = latitude + lat * distance; const upperLon = longitude + lon * distance; const lower = ngeohash_1.default.encode(lowerLat, lowerLon); const upper = ngeohash_1.default.encode(upperLat, upperLon); return { lower, upper, }; } getPath() { if (!this.options || !this.options.parent) { return this.repository.getClassName() + '/data/' + this.repository.getClassName().toLowerCase() + 's'; } let path = ''; this.options.parent.forEach((p) => { const parentClassName = p.modelName.toUpperCase().substr(0, 1) + p.modelName.toLowerCase().substr(1); path += parentClassName + '/data/' + parentClassName.toLowerCase() + 's' + '/'; path += p.documentId + '/'; path += this.repository.getClassName() + '/data/' + this.repository.getClassName().toLowerCase() + 's'; }); return path; } getParentDocumentPath() { const pathSegments = this.getPath().split('/'); return pathSegments.slice(0, pathSegments.length - 3).join('/'); } getFirebaseCollection(sql) { const ref = this.db.collection(this.getPath()); const parser1 = new Parser(); const ast = parser1.parse('SELECT * FROM t WHERE ' + sql); const addQuery = (reference, statement) => { if (!statement) { return reference; } if (statement.operator === 'AND') { if (statement.left) { reference = addQuery(reference, statement.left); } if (statement.right) { reference = addQuery(reference, statement.right); } } if (statement.left && statement.left.type === 'SimpleExprParentheses') { statement.left.value.value.forEach((v) => { reference = addQuery(reference, v); }); } if (statement.right && statement.right.type === 'SimpleExprParentheses') { statement.right.value.value.forEach((v) => { reference = addQuery(reference, v); }); } if (statement.operator === '=' || statement.operator === 'LIKE') { reference = reference.where(statement.left.type === 'Identifier' ? statement.left.value : statement.right.value, '==', statement.left.type === 'Identifier' ? this.evaluateAstValue(statement.right) : this.evaluateAstValue(statement.left)); } if (statement.operator === '>' || statement.operator === '<' || statement.operator === '<=' || statement.operator === '>=') { reference = reference.where(statement.left.type === 'Identifier' ? statement.left.value : statement.right.value, statement.operator, statement.left.type === 'Identifier' ? this.evaluateAstValue(statement.right) : this.evaluateAstValue(statement.left)); } if (statement.operator === 'IN') { reference = reference.where(statement.left.type === 'Identifier' ? statement.left.value : statement.right.value, 'in', statement.left.type === 'Identifier' ? this.evaluateAstValue(statement.right) : this.evaluateAstValue(statement.left)); } return reference; }; return addQuery(ref, ast['value']['where']); } evaluateAstValue(value) { if (value.type === 'String') { return value.value.substr(1, value.value.length - 2); } if (value.type === 'Boolean' && value.value === 'TRUE') { return true; } if (value.type === 'Boolean' && value.value === 'FALSE') { return false; } return value.value; } } exports.FirestoreConnector = FirestoreConnector;