UNPKG

@taraai/read-write

Version:

Synchronous NoSQL/Firestore for React

554 lines (457 loc) 14.7 kB
"use strict"; 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 } }); } }