@taraai/read-write
Version:
Synchronous NoSQL/Firestore for React
554 lines (457 loc) • 14.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.attachListener = attachListener;
exports.dataByIdSnapshot = dataByIdSnapshot;
exports.detachListener = detachListener;
exports.dispatchListenerResponse = dispatchListenerResponse;
exports.firestoreRef = firestoreRef;
exports.getBaseQueryName = getBaseQueryName;
exports.getPopulateChild = getPopulateChild;
exports.getQueryConfig = getQueryConfig;
exports.getQueryConfigs = getQueryConfigs;
exports.getQueryName = getQueryName;
exports.getSnapshotByObject = getSnapshotByObject;
exports.orderedFromSnap = orderedFromSnap;
exports.queryStrToObj = queryStrToObj;
exports.snapshotCache = void 0;
var _isObject = _interopRequireDefault(require("lodash/isObject"));
var _isNumber = _interopRequireDefault(require("lodash/isNumber"));
var _isEmpty = _interopRequireDefault(require("lodash/isEmpty"));
var _trim = _interopRequireDefault(require("lodash/trim"));
var _cloneDeep = _interopRequireDefault(require("lodash/cloneDeep"));
var _has = _interopRequireDefault(require("lodash/has"));
var _constants = require("../constants");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const snapshotCache = new WeakMap();
exports.snapshotCache = snapshotCache;
function getSnapshotByObject(obj) {
return snapshotCache.get(obj);
}
function addWhereToRef(ref, where) {
if (!Array.isArray(where)) {
throw new Error('where parameter must be an array.');
}
if (Array.isArray(where[0])) {
return where.reduce((acc, whereArgs) => addWhereToRef(acc, whereArgs), ref);
}
return ref.where(...where);
}
function addOrderByToRef(ref, orderBy) {
if (!Array.isArray(orderBy) && !(typeof orderBy === 'string' || orderBy instanceof String)) {
throw new Error('orderBy parameter must be an array or string.');
}
if (typeof orderBy === 'string' || orderBy instanceof String) {
return ref.orderBy(orderBy);
}
if (typeof orderBy[0] === 'string' || orderBy[0] instanceof String) {
return ref.orderBy(...orderBy);
}
return orderBy.reduce((acc, orderByArgs) => addOrderByToRef(acc, orderByArgs), ref);
}
function arrayify(cursor) {
return [].concat(cursor);
}
function handleSubcollections(ref, subcollectionList) {
if (Array.isArray(subcollectionList)) {
subcollectionList.forEach(subcollection => {
if (subcollection.collection) {
if (typeof ref.collection !== 'function') {
throw new Error(`Collection can only be run on a document. Check that query config for subcollection: "${subcollection.collection}" contains a doc parameter.`);
}
ref = ref.collection(subcollection.collection);
}
if (subcollection.id) ref = ref.doc(subcollection.id);
if (subcollection.doc) ref = ref.doc(subcollection.doc);
if (subcollection.where) ref = addWhereToRef(ref, subcollection.where);
if (subcollection.orderBy) {
ref = addOrderByToRef(ref, subcollection.orderBy);
}
if (subcollection.limit) ref = ref.limit(subcollection.limit);
if (subcollection.startAt) {
ref = ref.startAt(...arrayify(subcollection.startAt));
}
if (subcollection.startAfter) {
ref = ref.startAfter(...arrayify(subcollection.startAfter));
}
if (subcollection.endAt) {
ref = ref.endAt(...arrayify(subcollection.endAt));
}
if (subcollection.endBefore) {
ref = ref.endBefore(...arrayify(subcollection.endBefore));
}
ref = handleSubcollections(ref, subcollection.subcollections);
});
}
return ref;
}
function firestoreRef(firebase, meta) {
if (!firebase.firestore) {
throw new Error('Firestore must be required and initalized.');
}
const {
path,
collection,
collectionGroup,
id,
doc,
subcollections,
where,
orderBy,
limit,
startAt,
startAfter,
endAt,
endBefore
} = meta;
let ref = firebase.firestore();
const isInvalidGroup = collectionGroup ? path || collection : false;
const isInvalidQuery = !(doc || id) ? !path && !collection : false;
if (isInvalidGroup) {
throw new Error(`Reference cannot contain both Path and CollectionGroup.` + ` (recieved: ${JSON.stringify(meta)})`);
}
if (!collectionGroup && isInvalidQuery) {
throw new Error(`Query References must include a 'path' property.` + ` (recieved: ${JSON.stringify(meta)})`);
}
const {
globalDataConvertor
} = firebase && firebase._ && firebase._.config || {};
if (path || collection) ref = ref.collection(path || collection);
if (collectionGroup) ref = ref.collectionGroup(collectionGroup);
if (id || doc) ref = ref.doc(id || doc);
ref = handleSubcollections(ref, subcollections);
if (where) ref = addWhereToRef(ref, where);
if (orderBy) ref = addOrderByToRef(ref, orderBy);
if (limit) ref = ref.limit(limit);
if (startAt) ref = ref.startAt(...arrayify(startAt));
if (startAfter) ref = ref.startAfter(...arrayify(startAfter));
if (endAt) ref = ref.endAt(...arrayify(endAt));
if (endBefore) ref = ref.endBefore(...arrayify(endBefore));
if (globalDataConvertor) ref = ref.withConverter(globalDataConvertor);
return ref;
}
function arrayToStr(key, value) {
if (value instanceof Date || (0, _has.default)(value, '_seconds')) {
return `${key}=${new Intl.DateTimeFormat('en-US', {
dateStyle: 'short',
timeStyle: 'short',
hour12: false
}).format((0, _has.default)(value, '_seconds') ? new Date(value * 1000) : value)}`;
}
if (typeof value === 'string' || value instanceof String || (0, _isNumber.default)(value)) {
return `${key}=${value}`;
}
if (typeof value[0] === 'string' || value[0] instanceof String) {
return `${key}=${value.join(':')}`;
}
if (value && !Array.isArray(value) && typeof value.toString === 'function') {
return `${key}=${value.toString()}`;
}
return value.map(val => arrayToStr(key, val));
}
function pickQueryParams(obj) {
return ['where', 'orderBy', 'limit', 'startAfter', 'startAt', 'endAt', 'endBefore'].reduce((acc, key) => obj[key] ? { ...acc,
[key]: obj[key]
} : acc, {});
}
function serialize(queryParams) {
return Object.keys(queryParams).filter(key => queryParams[key] !== undefined).map(key => arrayToStr(key, queryParams[key])).join('&');
}
function getQueryName(meta) {
if (typeof meta === 'string' || meta instanceof String) {
return meta;
}
const {
path,
collection,
collectionGroup,
id,
doc,
subcollections,
storeAs,
...remainingMeta
} = meta;
if (!path && !collection && !collectionGroup) {
throw new Error('Path or Collection Group is required to build query name');
}
if (storeAs) {
return storeAs;
}
let basePath = path || collection || collectionGroup;
if (id || doc) {
basePath = basePath.concat(`/${id || doc}`);
}
if ((path || collection) && subcollections) {
const mappedCollections = subcollections.map(subcollection => getQueryName(subcollection));
basePath = `${basePath}/${mappedCollections.join('/')}`;
}
const queryParams = pickQueryParams(remainingMeta);
if (!(0, _isEmpty.default)(queryParams)) {
if (queryParams.where && !Array.isArray(queryParams.where)) {
throw new Error('where parameter must be an array.');
}
basePath = basePath.concat('?', serialize(queryParams));
}
return basePath;
}
function getBaseQueryName(meta) {
if (typeof meta === 'string' || meta instanceof String) {
return meta;
}
const {
path,
collection,
collectionGroup,
subcollections,
...remainingMeta
} = meta;
if (!path && !collection && !collectionGroup) {
throw new Error('Path or Collection Group is required to build query name');
}
let basePath = path || collection || collectionGroup;
if ((path || collection) && subcollections) {
const mappedCollections = subcollections.map(subcollection => getQueryName(subcollection));
basePath = `${basePath}/${mappedCollections.join('/')}`;
}
const queryParams = pickQueryParams(remainingMeta);
if (!(0, _isEmpty.default)(queryParams)) {
if (queryParams.where && !Array.isArray(queryParams.where)) {
throw new Error('where parameter must be an array.');
}
basePath = basePath.concat('?', serialize(queryParams));
}
return basePath;
}
function confirmMetaAndConfig(firebase, meta) {
if (!meta) {
throw new Error('Meta data is required to attach listener.');
}
if (!firebase || !firebase._ || !firebase._.listeners) {
throw new Error('Internal Firebase object required to attach listener. Confirm that reduxFirestore enhancer was added when you were creating your store');
}
}
function attachListener(firebase, dispatch, meta, unsubscribe) {
confirmMetaAndConfig(firebase, meta);
const name = getQueryName(meta);
if (!firebase._.listeners[name]) {
firebase._.listeners[name] = unsubscribe;
}
dispatch({
type: _constants.actionTypes.SET_LISTENER,
meta,
payload: {
name
}
});
return firebase._.listeners;
}
function detachListener(firebase, dispatch, meta) {
const name = getQueryName(meta);
if (firebase._.listeners[name]) {
firebase._.listeners[name]();
delete firebase._.listeners[name];
}
const {
preserveCacheAfterUnset: preserveCache
} = firebase._.config || {};
dispatch({
type: _constants.actionTypes.UNSET_LISTENER,
meta,
payload: {
name,
preserveCache
}
});
}
function queryStrToObj(queryPathStr, parsedPath) {
const pathArr = parsedPath || (0, _trim.default)(queryPathStr, ['/']).split('/');
const [collection, doc, ...subcollections] = pathArr;
const queryObj = {};
if (collection) queryObj.collection = collection;
if (doc) queryObj.doc = doc;
if (subcollections.length) {
queryObj.subcollections = [queryStrToObj('', subcollections)];
}
return queryObj;
}
function getQueryConfig(query) {
if (typeof query === 'string' || query instanceof String) {
return queryStrToObj(query);
}
if ((0, _isObject.default)(query)) {
if (!query.path && !query.id && !query.collection && !query.collectionGroup && !query.doc) {
throw new Error('Path, Collection Group and/or Id are required parameters within query definition object.');
}
return query;
}
throw new Error('Invalid Path Definition: Only Strings and Objects are accepted.');
}
function getQueryConfigs(queries) {
if (Array.isArray(queries)) {
return queries.map(getQueryConfig);
}
if (typeof queries === 'string' || queries instanceof String) {
return queryStrToObj(queries);
}
if ((0, _isObject.default)(queries)) {
return [getQueryConfig(queries)];
}
throw new Error('Querie(s) must be an Array or a string.');
}
function orderedFromSnap(snap) {
const ordered = [];
if (snap.data && snap.exists) {
const {
id,
ref: {
parent: {
path
}
}
} = snap;
const obj = (0, _isObject.default)(snap.data()) ? { ...(snap.data() || snap.data),
id,
path
} : {
id,
path,
data: snap.data()
};
snapshotCache.set(obj, snap);
ordered.push(obj);
} else if (snap.forEach) {
snap.forEach(doc => {
const {
id,
ref: {
parent: {
path
}
}
} = doc;
const obj = (0, _isObject.default)(doc.data()) ? { ...(doc.data() || doc.data),
id,
path
} : {
id,
path,
data: doc.data()
};
snapshotCache.set(obj, doc);
ordered.push(obj);
});
}
snapshotCache.set(ordered, snap);
return ordered;
}
function dataByIdSnapshot(snap) {
const data = {};
if (snap.data) {
const snapData = snap.exists ? snap.data() : null;
if (snapData) {
snapshotCache.set(snapData, snap);
data[snap.id] = { ...snapData,
id: snap.id,
path: snap.ref.parent.path
};
} else {
data[snap.id] = null;
}
} else if (snap.forEach) {
snap.forEach(doc => {
const snapData = doc.data() || doc;
snapshotCache.set(snapData, doc);
data[doc.id] = { ...snapData,
id: doc.id,
path: doc.ref.parent.path
};
});
}
if (!!data && Object.keys(data).length) {
snapshotCache.set(data, snap);
return data;
}
return null;
}
function getPopulateChild(firebase, populate, id) {
return firestoreRef(firebase, {
collection: populate.root,
doc: id
}).get().then(snap => ({
id,
...snap.data()
}));
}
const changeTypeToEventType = {
added: _constants.actionTypes.DOCUMENT_ADDED,
removed: _constants.actionTypes.DOCUMENT_REMOVED,
modified: _constants.actionTypes.DOCUMENT_MODIFIED
};
function docChangeEvent(change) {
let originalMeta = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
const meta = { ...(0, _cloneDeep.default)(originalMeta),
path: change.doc.ref.parent.path
};
if (originalMeta.subcollections && !originalMeta.storeAs) {
meta.subcollections[0] = { ...meta.subcollections[0],
doc: change.doc.id
};
} else {
meta.doc = change.doc.id;
}
const data = {
id: change.doc.id,
path: change.doc.ref.parent.path,
...change.doc.data()
};
return {
type: changeTypeToEventType[change.type] || _constants.actionTypes.DOCUMENT_MODIFIED,
meta,
payload: {
data,
ordered: {
oldIndex: change.oldIndex,
newIndex: change.newIndex
}
}
};
}
function dispatchListenerResponse(_ref) {
let {
dispatch,
docData = {},
meta,
firebase
} = _ref;
const {
mergeOrdered,
mergeOrderedDocUpdates,
mergeOrderedCollectionUpdates
} = firebase._.config || {};
const fromCache = docData && typeof docData.metadata.fromCache === 'boolean' ? docData.metadata.fromCache : true;
const docChanges = typeof docData.docChanges === 'function' ? docData.docChanges() : docData.docChanges;
if (docChanges && docChanges.length < docData.size) {
docChanges.forEach((change, index) => {
const lastChange = index === docChanges.length - 1;
dispatch(docChangeEvent(change, {
reprocess: lastChange,
...meta
}));
});
} else {
dispatch({
type: _constants.actionTypes.LISTENER_RESPONSE,
meta,
payload: {
data: dataByIdSnapshot(docData),
ordered: orderedFromSnap(docData),
fromCache
},
merge: {
docs: mergeOrdered && mergeOrderedDocUpdates,
collections: mergeOrdered && mergeOrderedCollectionUpdates
}
});
}
}