yalento
Version:
An awesome integration of Google Firebase for Angular and Node
375 lines (374 loc) • 16.9 kB
JavaScript
"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;