UNPKG

@taraai/read-write

Version:

Synchronous NoSQL/Firestore for React

308 lines (243 loc) 9.52 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.convertReadProviders = convertReadProviders; exports.default = mutate; exports.isProviderRead = exports.isDocRead = exports.getRead = void 0; var _chunk = _interopRequireDefault(require("lodash/chunk")); var _cloneDeep = _interopRequireDefault(require("lodash/cloneDeep")); var _flatten = _interopRequireDefault(require("lodash/flatten")); var _isFunction = _interopRequireDefault(require("lodash/isFunction")); var _isPlainObject = _interopRequireDefault(require("lodash/isPlainObject")); var _mapValues = _interopRequireDefault(require("lodash/mapValues")); var _isEmpty = _interopRequireDefault(require("lodash/isEmpty")); var _has = _interopRequireDefault(require("lodash/has")); var _debug = _interopRequireDefault(require("debug")); var _query = require("./query"); var _profiling = _interopRequireDefault(require("./profiling")); var _firestore = require("firebase/firestore"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const info = (0, _debug.default)('readwrite:mutate'); const docRef = (firestore, collection, doc) => firestore.doc(`${collection}/${doc}`); const promiseAllObject = async object => Object.fromEntries(await Promise.all(Object.entries(object).map(_ref => { let [key, promise] = _ref; return promise.then(value => [key, value]); }))); const isAsync = fnc => fnc.constructor.name === 'AsyncFunction'; const isBatchedWrite = operations => Array.isArray(operations); const isDocRead = function () { let { doc, id } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return typeof id === 'string' || typeof doc === 'string'; }; exports.isDocRead = isDocRead; const isProviderRead = read => (0, _has.default)(read, '::w3Provider') || (0, _isFunction.default)(read); exports.isProviderRead = isProviderRead; const getRead = read => { if ((0, _has.default)(read, '::w3Provider')) return read['::w3Provider']; if ((0, _isFunction.default)(read)) return read(); return read; }; exports.getRead = getRead; const isSingleWrite = function () { let { collection, path } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return typeof path === 'string' || typeof collection === 'string'; }; const hasNothing = snapshot => !snapshot || (0, _has.default)(snapshot, 'empty') && snapshot.empty() || (0, _has.default)(snapshot, 'exists') && snapshot.exists; const primaryValue = arr => Array.isArray(arr) && typeof arr[0] === 'string' && arr[0].indexOf('::') === 0 ? null : arr; const arrayUnion = function (firebase, key) { if (key !== '::arrayUnion') return null; for (var _len = arguments.length, val = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { val[_key - 2] = arguments[_key]; } return (0, _firestore.arrayUnion)(...val); }; const arrayRemove = function (firebase, key) { if (key !== '::arrayRemove') return null; for (var _len2 = arguments.length, val = new Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { val[_key2 - 2] = arguments[_key2]; } return (0, _firestore.arrayRemove)(...val); }; const increment = (firebase, key, val) => key === '::increment' && typeof val === 'number' && (0, _firestore.increment)(val); const deleteField = (firebase, key) => key === '::delete' && (0, _firestore.deleteField)(); const serverTimestamp = (firebase, key) => key === '::serverTimestamp' && (0, _firestore.serverTimestamp)(); function atomize(firebase, operation) { let requiresUpdate = false; return [Object.keys(operation).reduce((data, key) => { const clone = { ...data }; const val = clone[key]; if (key.includes('.')) { requiresUpdate = true; } if (!val) return clone; const value = primaryValue(val) || serverTimestamp(firebase, val[0]) || arrayUnion(firebase, val[0], val[1]) || arrayRemove(firebase, val[0], val[1]) || increment(firebase, val[0], val[1]) || deleteField(firebase, val[0], val[1]); if (Array.isArray(val) && val.length > 0 && (0, _isPlainObject.default)(val[0])) { clone[key] = val.map(obj => { const [object, update] = atomize(firebase, obj); if (update) requiresUpdate = true; return object; }); } else if (Array.isArray(val) && val.length > 0) { clone[key] = value; } else if ((0, _isPlainObject.default)(val)) { const [object, update] = atomize(firebase, val); clone[key] = object; if (update) requiresUpdate = true; } return clone; }, (0, _cloneDeep.default)(operation)), requiresUpdate]; } function write(firebase) { let operation = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; let writer = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; if ((0, _isEmpty.default)(operation)) return Promise.resolve(); const { collection, path, doc, id, data, ...rest } = operation; const ref = docRef(firebase.firestore(), path || collection, id || doc); const [changes, requiresUpdate = false] = atomize(firebase, data || rest); if (writer) { const writeType = writer.commit ? 'Batching' : 'Transaction.set'; if (info.enabled) { info(writeType, JSON.stringify({ id: ref.id, path: ref.parent.path, ...changes }, null, 2)); } if (requiresUpdate) { writer.update(ref, changes); } else { writer.set(ref, changes, { merge: true }); } return { id: ref.id, path: ref.parent.path, ...changes }; } if (info.enabled) { info('Writing', JSON.stringify({ id: ref.id, path: ref.parent.path, ...changes }, null, 2)); } if (requiresUpdate) { return ref.update(changes); } return ref.set(changes, { merge: true }); } function writeSingle(firebase, operations) { const done = (0, _profiling.default)('mutate.writeSingle'); const promise = write(firebase, operations); done(); return promise; } const MAX_BATCH_COUNT = 500; async function writeInBatch(firebase, operations) { const done = (0, _profiling.default)('mutate.writeInBatch'); const committedBatchesPromised = (0, _chunk.default)(operations, MAX_BATCH_COUNT).map(operationsChunk => { const batch = firebase.firestore().batch(); const writesBatched = operationsChunk.map(operation => write(firebase, operation, batch)); return batch.commit().then(() => writesBatched); }); done(); return Promise.all(committedBatchesPromised).then(_flatten.default); } async function writeInTransaction(firebase, operations) { return firebase.firestore().runTransaction(async transaction => { const serialize = doc => !doc ? null : { ...doc.data(), id: doc.ref.id, path: doc.ref.parent.path }; const getter = ref => { if (info.enabled) { info('Transaction.get ', { id: ref.id, path: ref.parent.path }); } return transaction.get(ref); }; const done = (0, _profiling.default)('mutate.writeInTransaction:reads'); const readsPromised = (0, _mapValues.default)(operations.reads, async read => { if (isProviderRead(read)) { return getRead(read); } if (isDocRead(read)) { const doc = (0, _query.firestoreRef)(firebase, read); const snapshot = await getter(doc); return serialize(hasNothing(snapshot) ? null : snapshot); } const query = (0, _query.firestoreRef)(firebase, read); const nonTransactionQuery = await query.get(); if (hasNothing(nonTransactionQuery) || nonTransactionQuery.docs.length === 0) return []; const transactionDocs = await Promise.all(nonTransactionQuery.docs.map(_ref2 => { let { ref } = _ref2; return getter(ref); })); return transactionDocs.map(serialize); }); done(); const reads = await promiseAllObject(readsPromised); const writes = []; operations.writes.forEach(writeFnc => { if (isAsync(writeFnc)) throw new Error('Writes must be synchronous, unary functions.'); const complete = (0, _profiling.default)('mutate.writeInTransaction:writes'); const operation = typeof writeFnc === 'function' ? writeFnc(reads) : writeFnc; if (Array.isArray(operation)) { operation.map(op => write(firebase, op, transaction)); writes.push(operation); } else { writes.push(write(firebase, operation, transaction)); } complete(); }); return { reads, writes }; }); } function convertReadProviders(mutations) { const shouldMakeProvidesIdempotent = mutations && mutations.reads; if (!shouldMakeProvidesIdempotent) return; Object.keys(mutations.reads).forEach(key => { const read = mutations.reads[key]; const isReadProvider = (0, _isFunction.default)(read); if (isAsync(read)) throw new Error('Read Providers must be synchronous, nullary functions.'); mutations.reads[key] = isReadProvider ? { '::w3Provider': read() } : read; }, mutations); } function mutate(firestore, operations) { if (isSingleWrite(operations)) { return writeSingle(firestore, operations); } if (isBatchedWrite(operations)) { return writeInBatch(firestore, operations); } convertReadProviders(operations); return writeInTransaction(firestore, operations); }