firestore-mock
Version:
Firestore intsance mock for the Firebase Admin SDK
400 lines (360 loc) • 10.4 kB
JavaScript
const util = require("util");
const CollectionReferenceMock = require("./CollectionReferenceMock");
const DatabaseMock = require("./DatabaseMock");
const TimestampMock = require("./TimestampMock");
/*
FirestoreMock class used for mocking the admin SDK FirestoreMock class. Can be stubbed in
the test by sinon or imported into a test file and used as a function param
*/
function FirestoreMock() {
this._db = new DatabaseMock();
this.app = "Mock app not supported";
}
FirestoreMock.prototype.firestore = function() {
return this;
};
FirestoreMock.prototype.collection = function(name) {
return new CollectionReferenceMock(name, this);
};
FirestoreMock.prototype._get = function(collection_id, id) {
if (
!this._db._collections[collection_id] ||
!this._db._collections[collection_id][id]
) {
return undefined;
} else {
return this._db._collections[collection_id][id];
}
};
FirestoreMock.prototype._set = function(collection_id, id, data, options) {
data = this._checkData(data, id);
if (!this._db._collections[collection_id]) {
this._db._collections[collection_id] = {};
}
if (
options &&
options["merge"] === "true" &&
this._db._collections[collection_id][id]
) {
this._update(collection_id, id, data);
return;
} else {
this._db._collections[collection_id][id] = data;
return;
}
};
FirestoreMock.prototype._update = function(collection_id, id, data) {
let serialized_data = this._checkData(data, id);
if (
!this._db._collections[collection_id] &&
!this._db._collections[collection_id][id]
) {
throw new Error("Document does not exist, failed to update");
} else {
let keys = Object.keys(serialized_data);
let doc = this._db._collections[collection_id][id];
for (let index in keys) {
doc[keys[index]] = serialized_data[keys[index]];
}
return;
}
};
FirestoreMock.prototype._delete = function(collection_id, id) {
if (this._db._collections[collection_id][id]) {
delete this._db._collections[collection_id][id];
}
return;
};
FirestoreMock.prototype._where = function(
field,
operator,
value,
docs,
collection_id
) {
if (!operators[operator]) {
throw new Error(
"Query.where() calls with this operator is not supported at this time"
);
}
let data;
if (docs && Object.keys(docs).length) {
data = docs;
} else if (collection_id) {
data = this._db._collections[collection_id]
? this._db._collections[collection_id]
: {};
} else {
data = this._db._collections;
}
let filtered_docs = {};
let keys = Object.keys(data);
if (!keys.length) {
return data;
}
for (let index in keys) {
let doc = data[keys[index]];
// comparing the field of each doc to the given value via the operator
if (operators[operator](doc[field], value)) {
filtered_docs[keys[index]] = doc;
}
}
return filtered_docs;
};
FirestoreMock.prototype._checkData = function(data, id) {
let keys = Object.keys(data);
let undefined_keys = keys.map(key => {
if (data[key] === undefined) {
return key;
}
});
undefined_keys = undefined_keys.filter(key => {
return key !== undefined;
});
if (undefined_keys.length) {
let names = undefined_keys.join(" ");
throw new Error(
`Document ${id} contains undefined key values: ${names}. This document cannot be saved to Firestore.`
);
}
return serializeObject(data);
};
FirestoreMock.prototype.clearData = function() {
delete this._db;
this._db = new DatabaseMock();
};
FirestoreMock.prototype.batch = function() {
return new WriteBatchMock(this);
};
FirestoreMock.prototype.runTransaction = async function(callback) {
let transaction = new TransactionMock(this);
await callback(transaction);
transaction.commit();
return;
};
function WriteBatchMock(firestore) {
this.firestore = firestore;
this._set = [];
this._update = [];
this._delete = [];
}
WriteBatchMock.prototype.set = function(ref, data, options) {
this._set.push({ ref, data, options });
};
WriteBatchMock.prototype.update = function(ref, data) {
this._update.push({ ref, data });
};
WriteBatchMock.prototype.delete = function(ref, data) {
this._delete.push({ ref, data });
};
WriteBatchMock.prototype.commit = function() {
for (let index in this._set) {
let params = this._set[index];
this.firestore._set(
params.ref.parent.id,
params.ref.id,
params.data,
params.options
);
}
for (let index in this._update) {
let params = this._update[index];
this.firestore._update(params.ref.parent.id, params.ref.id, params.data);
}
for (let index in this._delete) {
let params = this._delete[index];
this.firestore._delete(params.ref.parent.id, params.ref.id);
}
};
function TransactionMock(firestore) {
this.firestore = firestore;
this._set = [];
this._update = [];
this._delete = [];
}
util.inherits(TransactionMock, WriteBatchMock);
TransactionMock.prototype.get = function(ref) {
return ref.get();
};
function serializeObject(obj) {
let serialized_obj = {};
let keys = Object.keys(obj);
for (let index in keys) {
let key = keys[index];
let val = obj[key];
if (
val === null ||
(val.constructor !== Date &&
val.constructor !== Array &&
val.constructor !== Object)
) {
serialized_obj[key] = val;
} else if (val.constructor === Date) {
serialized_obj[key] = new TimestampMock(val);
} else if (val.constructor === Array) {
serialized_obj[key] = serializeArray(val);
} else {
serialized_obj[key] = serializeObject(val);
}
}
return serialized_obj;
}
function serializeArray(array) {
let serialized_array = [];
for (let index in array) {
let val = array[index];
if (val === null) {
serialized_array[index] = null;
} else if (val.constructor === Array) {
throw new Error(
"Document contains nested arrays and cannot be saved to Firestore"
);
} else if (val.constructor === Date) {
serialized_array[index] = new TimestampMock(val);
} else if (val.constructor === Array) {
serialized_array[index] = serializeArray(val);
} else if (val.constructor === Object) {
serialized_array[index] = serializeObject(val);
} else {
serialized_array[index] = val;
}
}
return serialized_array;
}
const operators = {
"==": function(field, value) {
if (field && field.constructor.name === "TimestampMock") {
try {
return field.date.getTime() === value.getTime();
} catch (err) {
throw new Error(
"A query was performed on a firebase Timestamp field without using a JavaScript Date object"
);
}
} else if (typeof field !== typeof value) {
return false;
} else {
return field === value;
}
},
"<=": function(field, value) {
if (field && field.constructor.name === "TimestampMock") {
try {
return field.date.getTime() <= value.getTime();
} catch (err) {
throw new Error(
"A query was performed on a firebase Timestamp field without using a JavaScript Date object"
);
}
} else if (typeof field !== typeof value) {
return false;
} else {
return field <= value;
}
},
">=": function(field, value) {
if (field && field.constructor.name === "TimestampMock") {
try {
return field.date.getTime() >= value.getTime();
} catch (err) {
throw new Error(
"A query was performed on a firebase Timestamp field without using a JavaScript Date object"
);
}
} else if (typeof field !== typeof value) {
return false;
} else {
return field >= value;
}
},
">": function(field, value) {
if (field && field.constructor.name === "TimestampMock") {
try {
return field.date.getTime() > value.getTime();
} catch (err) {
throw new Error(
"A query was performed on a firebase Timestamp field without using a JavaScript Date object"
);
}
} else if (typeof field !== typeof value) {
return false;
} else {
return field > value;
}
},
"<": function(field, value) {
if (field && field.constructor.name === "TimestampMock") {
try {
return field.date.getTime() < value.getTime();
} catch (err) {
throw new Error(
"A query was performed on a firebase Timestamp field without using a JavaScript Date object"
);
}
} else if (typeof field !== typeof value) {
return false;
} else {
return field < value;
}
},
"array-contains": function(field, value) {
if (!Array.isArray(field)) {
return false;
}
return field.includes(value);
},
in: function(field, values) {
if (!Array.isArray(values) || !values.length) {
throw new Error("The 'in' filter operator requires an array of values");
}
if (values.length > 10) {
throw new Error(
"Firestore only allows up to 10 values to be filtered in an 'in' filter"
);
}
if (field && field.constructor.name === "TimestampMock") {
for (let index in values) {
let val = values[index];
if (val instanceof Date && val.getTime() === field.date.getTime()) {
return true;
}
}
return false;
} else {
return values.includes(field);
}
},
"array-contains-any": function(field, values) {
if (!Array.isArray(values) || !values.length) {
throw new Error(
"The 'array-contains-any' filter operator requires an array of values"
);
}
if (values.length > 10) {
throw new Error(
"Firestore only allows up to 10 values to be filtered in an 'array-contains-any' filter"
);
}
if (!Array.isArray(field)) {
return false;
} else {
for (let index in values) {
let val = values[index];
if (val instanceof Date) {
let time = val.getTime();
for (let index in field) {
if (
field[index] instanceof TimestampMock &&
field[index].date.getTime() === time
) {
return true;
}
}
} else {
return field.includes(val);
}
}
}
}
};
module.exports = FirestoreMock;