@taraai/read-write
Version:
Synchronous NoSQL/Firestore for React
308 lines (243 loc) • 9.52 kB
JavaScript
;
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);
}