firestore-vitest
Version:
Vitest helper for mocking Google Cloud Firestore
288 lines (256 loc) • 8.44 kB
JavaScript
import buildDocFromHash from './buildDocFromHash.js';
export default 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 && record[key] !== undefined);
}
function _recordsWithNonNullKey(records, key) {
return records.filter(record => record && record[key] !== undefined && 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 => {
if (_shouldCompareNumerically(record[key], value)) {
return record[key] < value;
}
if (_shouldCompareTimestamp(record[key], value)) {
return record[key].toMillis() < value;
}
return String(record[key]) < 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 => {
if (_shouldCompareNumerically(record[key], value)) {
return record[key] <= value;
}
if (_shouldCompareTimestamp(record[key], value)) {
return record[key].toMillis() <= value;
}
return String(record[key]) <= 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 => {
if (_shouldCompareTimestamp(record[key], value)) {
//NOTE: for equality, we must compare numbers!
return record[key].toMillis() === value.getTime();
}
return String(record[key]) === 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 => {
if (_shouldCompareTimestamp(record[key], value)) {
//NOTE: for equality, we must compare numbers!
return record[key].toMillis() !== value.getTime();
}
return String(record[key]) !== 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 => {
if (_shouldCompareNumerically(record[key], value)) {
return record[key] >= value;
}
if (_shouldCompareTimestamp(record[key], value)) {
return record[key].toMillis() >= value;
}
return String(record[key]) >= 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 => {
if (_shouldCompareNumerically(record[key], value)) {
return record[key] > value;
}
if (_shouldCompareTimestamp(record[key], value)) {
return record[key].toMillis() > value;
}
return String(record[key]) > 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 && record[key] && Array.isArray(record[key]) && 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 || record[key] === undefined) {
return false;
}
return value && Array.isArray(value) && value.includes(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(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 &&
record[key] &&
Array.isArray(record[key]) &&
value &&
Array.isArray(value) &&
record[key].some(v => value.includes(v)),
);
}