firestore-jest-mock
Version:
Jest helper for mocking Google Cloud Firestore
262 lines • 9.71 kB
JavaScript
;
const buildDocFromHash = require('./buildDocFromHash');
module.exports = function buildQuerySnapShot(requestedRecords, filters, selectFields) {
const definiteRecords = requestedRecords.filter(rec => !!rec);
const results = _filteredDocuments(definiteRecords, filters);
const docs = results.map(doc => buildDocFromHash(doc, 'abc123', selectFields));
return {
empty: results.length < 1,
size: results.length,
docs,
forEach(callback) {
docs.forEach(callback);
},
docChanges() {
return [];
},
};
};
/**
* @typedef DocumentHash
* @type {import('./buildDocFromHash').DocumentHash}
*/
/**
* @typedef Comparator
* @type {import('./buildQuerySnapShot').Comparator}
*/
/**
* Applies query filters to an array of mock document data.
*
* @param {Array<DocumentHash>} records The array of records to filter.
* @param {Array<{ key: string; comp: Comparator; value: unknown }>=} filters The filters to apply.
* If no filters are provided, then the records array is returned as-is.
*
* @returns {Array<import('./buildDocFromHash').DocumentHash>} The filtered documents.
*/
function _filteredDocuments(records, filters) {
if (!filters || !Array.isArray(filters) || filters.length === 0) {
return records;
}
filters.forEach(({ key, comp, value }) => {
// https://firebase.google.com/docs/reference/js/firebase.firestore#wherefilterop
// Convert values to string to make Array comparisons work
// See https://jsbin.com/bibawaf/edit?js,console
switch (comp) {
// https://firebase.google.com/docs/firestore/query-data/queries#query_operators
case '<':
records = _recordsLessThanValue(records, key, value);
break;
case '<=':
records = _recordsLessThanOrEqualToValue(records, key, value);
break;
case '==':
records = _recordsEqualToValue(records, key, value);
break;
case '!=':
records = _recordsNotEqualToValue(records, key, value);
break;
case '>=':
records = _recordsGreaterThanOrEqualToValue(records, key, value);
break;
case '>':
records = _recordsGreaterThanValue(records, key, value);
break;
case 'array-contains':
records = _recordsArrayContainsValue(records, key, value);
break;
case 'in':
records = _recordsWithValueInList(records, key, value);
break;
case 'not-in':
records = _recordsWithValueNotInList(records, key, value);
break;
case 'array-contains-any':
records = _recordsWithOneOfValues(records, key, value);
break;
}
});
return records;
}
function _recordsWithKey(records, key) {
return records.filter(record => record && getValueByPath(record, key) !== undefined);
}
function _recordsWithNonNullKey(records, key) {
return records.filter(record => record && getValueByPath(record, key) !== undefined && getValueByPath(record, key) !== null);
}
function _shouldCompareNumerically(a, b) {
return typeof a === 'number' && typeof b === 'number';
}
function _shouldCompareTimestamp(a, b) {
//We check whether toMillis method exists to support both Timestamp mock and Firestore Timestamp object
//B is expected to be Date, not Timestamp, just like Firestore does
return (typeof a === 'object' && a !== null && typeof a.toMillis === 'function' && b instanceof Date);
}
/**
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsLessThanValue(records, key, value) {
return _recordsWithNonNullKey(records, key).filter(record => {
const recordValue = getValueByPath(record, key);
if (_shouldCompareNumerically(recordValue, value)) {
return recordValue < value;
}
if (_shouldCompareTimestamp(recordValue, value)) {
return recordValue.toMillis() < value;
}
return String(recordValue) < String(value);
});
}
/**
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsLessThanOrEqualToValue(records, key, value) {
return _recordsWithNonNullKey(records, key).filter(record => {
const recordValue = getValueByPath(record, key);
if (_shouldCompareNumerically(recordValue, value)) {
return recordValue <= value;
}
if (_shouldCompareTimestamp(recordValue, value)) {
return recordValue.toMillis() <= value;
}
return String(recordValue) <= String(value);
});
}
/**
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsEqualToValue(records, key, value) {
return _recordsWithKey(records, key).filter(record => {
const recordValue = getValueByPath(record, key);
if (_shouldCompareTimestamp(recordValue, value)) {
//NOTE: for equality, we must compare numbers!
return recordValue.toMillis() === value.getTime();
}
return String(recordValue) === String(value);
});
}
/**
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsNotEqualToValue(records, key, value) {
return _recordsWithKey(records, key).filter(record => {
const recordValue = getValueByPath(record, key);
if (_shouldCompareTimestamp(recordValue, value)) {
//NOTE: for equality, we must compare numbers!
return recordValue.toMillis() !== value.getTime();
}
return String(recordValue) !== String(value);
});
}
/**
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsGreaterThanOrEqualToValue(records, key, value) {
return _recordsWithNonNullKey(records, key).filter(record => {
const recordValue = getValueByPath(record, key);
if (_shouldCompareNumerically(recordValue, value)) {
return recordValue >= value;
}
if (_shouldCompareTimestamp(recordValue, value)) {
return recordValue.toMillis() >= value;
}
return String(recordValue) >= String(value);
});
}
/**
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsGreaterThanValue(records, key, value) {
return _recordsWithNonNullKey(records, key).filter(record => {
const recordValue = getValueByPath(record, key);
if (_shouldCompareNumerically(recordValue, value)) {
return recordValue > value;
}
if (_shouldCompareTimestamp(recordValue, value)) {
return recordValue.toMillis() > value;
}
return String(recordValue) > String(value);
});
}
/**
* @see https://firebase.google.com/docs/firestore/query-data/queries#array_membership
*
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsArrayContainsValue(records, key, value) {
return records.filter(record => record &&
getValueByPath(record, key) &&
Array.isArray(getValueByPath(record, key)) &&
getValueByPath(record, key).includes(value));
}
/**
* @see https://firebase.google.com/docs/firestore/query-data/queries#in_not-in_and_array-contains-any
*
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsWithValueInList(records, key, value) {
// TODO: Throw an error when a value is passed that contains more than 10 values
return records.filter(record => {
if (!record || getValueByPath(record, key) === undefined) {
return false;
}
return value && Array.isArray(value) && value.includes(getValueByPath(record, key));
});
}
/**
* @see https://firebase.google.com/docs/firestore/query-data/queries#not-in
*
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsWithValueNotInList(records, key, value) {
// TODO: Throw an error when a value is passed that contains more than 10 values
return _recordsWithKey(records, key).filter(record => value && Array.isArray(value) && !value.includes(getValueByPath(record, key)));
}
/**
* @see https://firebase.google.com/docs/firestore/query-data/queries#in_not-in_and_array-contains-any
*
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsWithOneOfValues(records, key, value) {
// TODO: Throw an error when a value is passed that contains more than 10 values
return records.filter(record => record &&
getValueByPath(record, key) &&
Array.isArray(getValueByPath(record, key)) &&
value &&
Array.isArray(value) &&
getValueByPath(record, key).some(v => value.includes(v)));
}
function getValueByPath(record, path) {
const keys = path.split('.');
return keys.reduce((nestedObject = {}, key) => nestedObject[key], record);
}
//# sourceMappingURL=buildQuerySnapShot.js.map