firestore-node-mock 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +479 -0
- package/index.d.ts +6 -0
- package/index.d.ts.map +1 -0
- package/index.js +6 -0
- package/index.js.map +1 -0
- package/mocks/auth.d.ts +34 -0
- package/mocks/auth.js +69 -0
- package/mocks/fieldValue.d.ts +23 -0
- package/mocks/fieldValue.js +57 -0
- package/mocks/firebase.d.ts +37 -0
- package/mocks/firebase.js +79 -0
- package/mocks/firestore.d.ts +181 -0
- package/mocks/firestore.js +610 -0
- package/mocks/googleCloudFirestore.d.ts +20 -0
- package/mocks/googleCloudFirestore.js +43 -0
- package/mocks/helpers/buildDocFromHash.d.ts +28 -0
- package/mocks/helpers/buildDocFromHash.js +73 -0
- package/mocks/helpers/buildQuerySnapShot.d.ts +29 -0
- package/mocks/helpers/buildQuerySnapShot.js +306 -0
- package/mocks/helpers/defaultMockOptions.d.ts +3 -0
- package/mocks/helpers/defaultMockOptions.js +5 -0
- package/mocks/path.d.ts +26 -0
- package/mocks/path.js +39 -0
- package/mocks/query.d.ts +32 -0
- package/mocks/query.js +162 -0
- package/mocks/reactNativeFirebaseFirestore.d.ts +20 -0
- package/mocks/reactNativeFirebaseFirestore.js +43 -0
- package/mocks/timestamp.d.ts +22 -0
- package/mocks/timestamp.js +103 -0
- package/mocks/transaction.d.ts +22 -0
- package/mocks/transaction.js +62 -0
- package/package.json +61 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as timestamp from '../timestamp.js';
|
|
2
|
+
import pkg from 'lodash';
|
|
3
|
+
const { merge } = pkg;
|
|
4
|
+
|
|
5
|
+
export default function buildDocFromHash(hash = {}, id = 'abc123', selectFields = undefined) {
|
|
6
|
+
const exists = !!hash || false;
|
|
7
|
+
return {
|
|
8
|
+
createTime: (hash && hash._createTime) || timestamp.Timestamp.now(),
|
|
9
|
+
exists,
|
|
10
|
+
id: (hash && hash.id) || id,
|
|
11
|
+
readTime: hash && hash._readTime,
|
|
12
|
+
ref: hash && hash._ref,
|
|
13
|
+
metadata: {
|
|
14
|
+
hasPendingWrites: 'Server',
|
|
15
|
+
},
|
|
16
|
+
updateTime: hash && hash._updateTime,
|
|
17
|
+
data() {
|
|
18
|
+
if (!exists) {
|
|
19
|
+
// From Firestore docs: "Returns 'undefined' if the document doesn't exist."
|
|
20
|
+
// See https://firebase.google.com/docs/reference/js/firestore_.documentsnapshot#documentsnapshotdata
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
let copy = { ...hash };
|
|
24
|
+
if (!hash._ref.parent.firestore.options.includeIdsInData) {
|
|
25
|
+
delete copy.id;
|
|
26
|
+
}
|
|
27
|
+
delete copy._collections;
|
|
28
|
+
delete copy._createTime;
|
|
29
|
+
delete copy._readTime;
|
|
30
|
+
delete copy._ref;
|
|
31
|
+
delete copy._updateTime;
|
|
32
|
+
|
|
33
|
+
if (selectFields !== undefined) {
|
|
34
|
+
copy = selectFields.reduce((acc, field) => {
|
|
35
|
+
const path = field.split('.');
|
|
36
|
+
return merge(acc, buildDocFromPath(copy, path));
|
|
37
|
+
}, {});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return copy;
|
|
41
|
+
},
|
|
42
|
+
get(fieldPath) {
|
|
43
|
+
// The field path can be compound: from the firestore docs
|
|
44
|
+
// fieldPath The path (e.g. 'foo' or 'foo.bar') to a specific field.
|
|
45
|
+
const parts = fieldPath.split('.');
|
|
46
|
+
const data = this.data();
|
|
47
|
+
return parts.reduce((acc, part, index) => {
|
|
48
|
+
const value = acc[part];
|
|
49
|
+
// if no key is found
|
|
50
|
+
if (value === undefined) {
|
|
51
|
+
// return null if we are on the last item in parts
|
|
52
|
+
// otherwise, return an empty object, so we can continue to iterate
|
|
53
|
+
return parts.length - 1 === index ? null : {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// if there is a value, return it
|
|
57
|
+
return value;
|
|
58
|
+
}, data);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function buildDocFromPath(data, path) {
|
|
64
|
+
if (data === undefined || data === null) {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const [root, ...subPath] = path;
|
|
69
|
+
const rootData = data[root];
|
|
70
|
+
return {
|
|
71
|
+
[root]: subPath.length ? buildDocFromPath(rootData, subPath) : rootData
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MockedDocument, DocumentHash } from './buildDocFromHash.js';
|
|
2
|
+
|
|
3
|
+
export type Comparator = '<' | '<=' | '==' | '!=' | '>=' | '>' | 'array-contains' | 'in' | 'not-in' | 'array-contains-any';
|
|
4
|
+
|
|
5
|
+
export interface QueryFilter {
|
|
6
|
+
key: string;
|
|
7
|
+
comp: Comparator;
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface MockedQuerySnapshot<Doc = MockedDocument> {
|
|
12
|
+
empty: boolean;
|
|
13
|
+
size: number;
|
|
14
|
+
docs: Array<Doc>;
|
|
15
|
+
forEach(callbackfn: (value: Doc, index: number, array: Array<Doc>) => void): void;
|
|
16
|
+
docChanges(): Array<never>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Builds a query result from the given array of record objects.
|
|
21
|
+
*
|
|
22
|
+
* @param requestedRecords
|
|
23
|
+
* @param filters
|
|
24
|
+
*/
|
|
25
|
+
export default function buildQuerySnapShot(
|
|
26
|
+
requestedRecords: Array<DocumentHash>,
|
|
27
|
+
filters?: Array<QueryFilter>,
|
|
28
|
+
selectFields?: string[],
|
|
29
|
+
): MockedQuerySnapshot;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import buildDocFromHash from './buildDocFromHash.js';
|
|
2
|
+
|
|
3
|
+
export default function buildQuerySnapShot(requestedRecords, filters, selectFields) {
|
|
4
|
+
const definiteRecords = requestedRecords.filter(rec => !!rec);
|
|
5
|
+
const results = _filteredDocuments(definiteRecords, filters);
|
|
6
|
+
const docs = results.map(doc => buildDocFromHash(doc, 'abc123', selectFields));
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
empty: results.length < 1,
|
|
10
|
+
size: results.length,
|
|
11
|
+
docs,
|
|
12
|
+
forEach(callback) {
|
|
13
|
+
docs.forEach(callback);
|
|
14
|
+
},
|
|
15
|
+
docChanges() {
|
|
16
|
+
return [];
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef DocumentHash
|
|
23
|
+
* @type {import('./buildDocFromHash.js').DocumentHash}
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef Comparator
|
|
28
|
+
* @type {import('./buildQuerySnapShot.js').Comparator}
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Applies query filters to an array of mock document data.
|
|
33
|
+
*
|
|
34
|
+
* @param {Array<DocumentHash>} records The array of records to filter.
|
|
35
|
+
* @param {Array<{ key: string; comp: Comparator; value: unknown }>=} filters The filters to apply.
|
|
36
|
+
* If no filters are provided, then the records array is returned as-is.
|
|
37
|
+
*
|
|
38
|
+
* @returns {Array<import('./buildDocFromHash.js').DocumentHash>} The filtered documents.
|
|
39
|
+
*/
|
|
40
|
+
function _filteredDocuments(records, filters) {
|
|
41
|
+
if (!filters || !Array.isArray(filters) || filters.length === 0) {
|
|
42
|
+
return records;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
filters.forEach(({ key, comp, value }) => {
|
|
46
|
+
// https://firebase.google.com/docs/reference/js/firebase.firestore#wherefilterop
|
|
47
|
+
// Convert values to string to make Array comparisons work
|
|
48
|
+
// See https://jsbin.com/bibawaf/edit?js,console
|
|
49
|
+
|
|
50
|
+
switch (comp) {
|
|
51
|
+
// https://firebase.google.com/docs/firestore/query-data/queries#query_operators
|
|
52
|
+
case '<':
|
|
53
|
+
records = _recordsLessThanValue(records, key, value);
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
case '<=':
|
|
57
|
+
records = _recordsLessThanOrEqualToValue(records, key, value);
|
|
58
|
+
break;
|
|
59
|
+
|
|
60
|
+
case '==':
|
|
61
|
+
records = _recordsEqualToValue(records, key, value);
|
|
62
|
+
break;
|
|
63
|
+
|
|
64
|
+
case '!=':
|
|
65
|
+
records = _recordsNotEqualToValue(records, key, value);
|
|
66
|
+
break;
|
|
67
|
+
|
|
68
|
+
case '>=':
|
|
69
|
+
records = _recordsGreaterThanOrEqualToValue(records, key, value);
|
|
70
|
+
break;
|
|
71
|
+
|
|
72
|
+
case '>':
|
|
73
|
+
records = _recordsGreaterThanValue(records, key, value);
|
|
74
|
+
break;
|
|
75
|
+
|
|
76
|
+
case 'array-contains':
|
|
77
|
+
records = _recordsArrayContainsValue(records, key, value);
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
case 'in':
|
|
81
|
+
records = _recordsWithValueInList(records, key, value);
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case 'not-in':
|
|
85
|
+
records = _recordsWithValueNotInList(records, key, value);
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
case 'array-contains-any':
|
|
89
|
+
records = _recordsWithOneOfValues(records, key, value);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return records;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _recordsWithKey(records, key) {
|
|
98
|
+
return records.filter(record => record && getValueByPath(record, key) !== undefined);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function _recordsWithNonNullKey(records, key) {
|
|
102
|
+
return records.filter(
|
|
103
|
+
record =>
|
|
104
|
+
record && getValueByPath(record, key) !== undefined && getValueByPath(record, key) !== null,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function _shouldCompareNumerically(a, b) {
|
|
109
|
+
return typeof a === 'number' && typeof b === 'number';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _shouldCompareTimestamp(a, b) {
|
|
113
|
+
//We check whether toMillis method exists to support both Timestamp mock and Firestore Timestamp object
|
|
114
|
+
//B is expected to be Date, not Timestamp, just like Firestore does
|
|
115
|
+
return (
|
|
116
|
+
typeof a === 'object' && a !== null && typeof a.toMillis === 'function' && b instanceof Date
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {Array<DocumentHash>} records
|
|
122
|
+
* @param {string} key
|
|
123
|
+
* @param {unknown} value
|
|
124
|
+
* @returns {Array<DocumentHash>}
|
|
125
|
+
*/
|
|
126
|
+
function _recordsLessThanValue(records, key, value) {
|
|
127
|
+
return _recordsWithNonNullKey(records, key).filter(record => {
|
|
128
|
+
const recordValue = getValueByPath(record, key);
|
|
129
|
+
if (_shouldCompareNumerically(recordValue, value)) {
|
|
130
|
+
return recordValue < value;
|
|
131
|
+
}
|
|
132
|
+
if (_shouldCompareTimestamp(recordValue, value)) {
|
|
133
|
+
return recordValue.toMillis() < value;
|
|
134
|
+
}
|
|
135
|
+
return String(recordValue) < String(value);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {Array<DocumentHash>} records
|
|
141
|
+
* @param {string} key
|
|
142
|
+
* @param {unknown} value
|
|
143
|
+
* @returns {Array<DocumentHash>}
|
|
144
|
+
*/
|
|
145
|
+
function _recordsLessThanOrEqualToValue(records, key, value) {
|
|
146
|
+
return _recordsWithNonNullKey(records, key).filter(record => {
|
|
147
|
+
const recordValue = getValueByPath(record, key);
|
|
148
|
+
if (_shouldCompareNumerically(recordValue, value)) {
|
|
149
|
+
return recordValue <= value;
|
|
150
|
+
}
|
|
151
|
+
if (_shouldCompareTimestamp(recordValue, value)) {
|
|
152
|
+
return recordValue.toMillis() <= value;
|
|
153
|
+
}
|
|
154
|
+
return String(recordValue) <= String(value);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {Array<DocumentHash>} records
|
|
160
|
+
* @param {string} key
|
|
161
|
+
* @param {unknown} value
|
|
162
|
+
* @returns {Array<DocumentHash>}
|
|
163
|
+
*/
|
|
164
|
+
function _recordsEqualToValue(records, key, value) {
|
|
165
|
+
return _recordsWithKey(records, key).filter(record => {
|
|
166
|
+
const recordValue = getValueByPath(record, key);
|
|
167
|
+
if (_shouldCompareTimestamp(recordValue, value)) {
|
|
168
|
+
//NOTE: for equality, we must compare numbers!
|
|
169
|
+
return recordValue.toMillis() === value.getTime();
|
|
170
|
+
}
|
|
171
|
+
return String(recordValue) === String(value);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {Array<DocumentHash>} records
|
|
177
|
+
* @param {string} key
|
|
178
|
+
* @param {unknown} value
|
|
179
|
+
* @returns {Array<DocumentHash>}
|
|
180
|
+
*/
|
|
181
|
+
function _recordsNotEqualToValue(records, key, value) {
|
|
182
|
+
return _recordsWithKey(records, key).filter(record => {
|
|
183
|
+
const recordValue = getValueByPath(record, key);
|
|
184
|
+
if (_shouldCompareTimestamp(recordValue, value)) {
|
|
185
|
+
//NOTE: for equality, we must compare numbers!
|
|
186
|
+
return recordValue.toMillis() !== value.getTime();
|
|
187
|
+
}
|
|
188
|
+
return String(recordValue) !== String(value);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {Array<DocumentHash>} records
|
|
194
|
+
* @param {string} key
|
|
195
|
+
* @param {unknown} value
|
|
196
|
+
* @returns {Array<DocumentHash>}
|
|
197
|
+
*/
|
|
198
|
+
function _recordsGreaterThanOrEqualToValue(records, key, value) {
|
|
199
|
+
return _recordsWithNonNullKey(records, key).filter(record => {
|
|
200
|
+
const recordValue = getValueByPath(record, key);
|
|
201
|
+
if (_shouldCompareNumerically(recordValue, value)) {
|
|
202
|
+
return recordValue >= value;
|
|
203
|
+
}
|
|
204
|
+
if (_shouldCompareTimestamp(recordValue, value)) {
|
|
205
|
+
return recordValue.toMillis() >= value;
|
|
206
|
+
}
|
|
207
|
+
return String(recordValue) >= String(value);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {Array<DocumentHash>} records
|
|
213
|
+
* @param {string} key
|
|
214
|
+
* @param {unknown} value
|
|
215
|
+
* @returns {Array<DocumentHash>}
|
|
216
|
+
*/
|
|
217
|
+
function _recordsGreaterThanValue(records, key, value) {
|
|
218
|
+
return _recordsWithNonNullKey(records, key).filter(record => {
|
|
219
|
+
const recordValue = getValueByPath(record, key);
|
|
220
|
+
if (_shouldCompareNumerically(recordValue, value)) {
|
|
221
|
+
return recordValue > value;
|
|
222
|
+
}
|
|
223
|
+
if (_shouldCompareTimestamp(recordValue, value)) {
|
|
224
|
+
return recordValue.toMillis() > value;
|
|
225
|
+
}
|
|
226
|
+
return String(recordValue) > String(value);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @see https://firebase.google.com/docs/firestore/query-data/queries#array_membership
|
|
232
|
+
*
|
|
233
|
+
* @param {Array<DocumentHash>} records
|
|
234
|
+
* @param {string} key
|
|
235
|
+
* @param {unknown} value
|
|
236
|
+
* @returns {Array<DocumentHash>}
|
|
237
|
+
*/
|
|
238
|
+
function _recordsArrayContainsValue(records, key, value) {
|
|
239
|
+
return records.filter(
|
|
240
|
+
record =>
|
|
241
|
+
record &&
|
|
242
|
+
getValueByPath(record, key) &&
|
|
243
|
+
Array.isArray(getValueByPath(record, key)) &&
|
|
244
|
+
getValueByPath(record, key).includes(value),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @see https://firebase.google.com/docs/firestore/query-data/queries#in_not-in_and_array-contains-any
|
|
250
|
+
*
|
|
251
|
+
* @param {Array<DocumentHash>} records
|
|
252
|
+
* @param {string} key
|
|
253
|
+
* @param {unknown} value
|
|
254
|
+
* @returns {Array<DocumentHash>}
|
|
255
|
+
*/
|
|
256
|
+
function _recordsWithValueInList(records, key, value) {
|
|
257
|
+
// TODO: Throw an error when a value is passed that contains more than 10 values
|
|
258
|
+
return records.filter(record => {
|
|
259
|
+
if (!record || getValueByPath(record, key) === undefined) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
return value && Array.isArray(value) && value.includes(getValueByPath(record, key));
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @see https://firebase.google.com/docs/firestore/query-data/queries#not-in
|
|
268
|
+
*
|
|
269
|
+
* @param {Array<DocumentHash>} records
|
|
270
|
+
* @param {string} key
|
|
271
|
+
* @param {unknown} value
|
|
272
|
+
* @returns {Array<DocumentHash>}
|
|
273
|
+
*/
|
|
274
|
+
function _recordsWithValueNotInList(records, key, value) {
|
|
275
|
+
// TODO: Throw an error when a value is passed that contains more than 10 values
|
|
276
|
+
return _recordsWithKey(records, key).filter(
|
|
277
|
+
record => value && Array.isArray(value) && !value.includes(getValueByPath(record, key)),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @see https://firebase.google.com/docs/firestore/query-data/queries#in_not-in_and_array-contains-any
|
|
283
|
+
*
|
|
284
|
+
* @param {Array<DocumentHash>} records
|
|
285
|
+
* @param {string} key
|
|
286
|
+
* @param {unknown} value
|
|
287
|
+
* @returns {Array<DocumentHash>}
|
|
288
|
+
*/
|
|
289
|
+
function _recordsWithOneOfValues(records, key, value) {
|
|
290
|
+
// TODO: Throw an error when a value is passed that contains more than 10 values
|
|
291
|
+
return records.filter(
|
|
292
|
+
record =>
|
|
293
|
+
record &&
|
|
294
|
+
getValueByPath(record, key) &&
|
|
295
|
+
Array.isArray(getValueByPath(record, key)) &&
|
|
296
|
+
value &&
|
|
297
|
+
Array.isArray(value) &&
|
|
298
|
+
getValueByPath(record, key).some(v => value.includes(v)),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getValueByPath(record, path) {
|
|
303
|
+
const keys = path.split('.');
|
|
304
|
+
return keys.reduce((nestedObject = {}, key) => nestedObject[key], record);
|
|
305
|
+
}
|
|
306
|
+
|
package/mocks/path.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @private
|
|
3
|
+
* @class
|
|
4
|
+
*/
|
|
5
|
+
declare abstract class Path<T> {
|
|
6
|
+
protected readonly segments: string[];
|
|
7
|
+
/**
|
|
8
|
+
* @private
|
|
9
|
+
* @hideconstructor
|
|
10
|
+
* @param segments
|
|
11
|
+
*/
|
|
12
|
+
constructor(segments: string[]);
|
|
13
|
+
compareTo(other: Path<T>): number;
|
|
14
|
+
toArray(): string[];
|
|
15
|
+
isEqual(other: Path<T>): boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @class
|
|
20
|
+
*/
|
|
21
|
+
export declare class FieldPath extends Path<FieldPath> implements FieldPath {
|
|
22
|
+
private static _DOCUMENT_ID;
|
|
23
|
+
constructor(...segments: string[]);
|
|
24
|
+
static documentId(): FieldPath;
|
|
25
|
+
isEqual(other: FieldPath): boolean;
|
|
26
|
+
}
|
package/mocks/path.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export class Path {
|
|
2
|
+
constructor(segments) {
|
|
3
|
+
this.segments = segments;
|
|
4
|
+
}
|
|
5
|
+
compareTo(other) {
|
|
6
|
+
const len = Math.min(this.segments.length, other.segments.length);
|
|
7
|
+
for (let i = 0; i < len; i++) {
|
|
8
|
+
if (this.segments[i] < other.segments[i]) {
|
|
9
|
+
return -1;
|
|
10
|
+
}
|
|
11
|
+
if (this.segments[i] > other.segments[i]) {
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (this.segments.length < other.segments.length) {
|
|
16
|
+
return -1;
|
|
17
|
+
}
|
|
18
|
+
if (this.segments.length > other.segments.length) {
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
isEqual(other) {
|
|
24
|
+
return this === other || this.compareTo(other) === 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class FieldPath extends Path {
|
|
29
|
+
constructor(...segments) {
|
|
30
|
+
super(segments);
|
|
31
|
+
}
|
|
32
|
+
static documentId() {
|
|
33
|
+
return FieldPath._DOCUMENT_ID;
|
|
34
|
+
}
|
|
35
|
+
isEqual(other) {
|
|
36
|
+
return super.isEqual(other);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
FieldPath._DOCUMENT_ID = new FieldPath('__name__');
|
package/mocks/query.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Mock } from 'node:test';
|
|
2
|
+
import type { FakeFirestore } from './firestore.js';
|
|
3
|
+
import type { MockedQuerySnapshot } from './helpers/buildQuerySnapShot.js';
|
|
4
|
+
|
|
5
|
+
export class Query {
|
|
6
|
+
constructor(collectionName: string, firestore: typeof FakeFirestore);
|
|
7
|
+
|
|
8
|
+
get(): Promise<MockedQuerySnapshot>;
|
|
9
|
+
select(): Query;
|
|
10
|
+
where(): Query;
|
|
11
|
+
offset(): Query;
|
|
12
|
+
limit(): Query;
|
|
13
|
+
orderBy(): Query;
|
|
14
|
+
startAfter(): Query;
|
|
15
|
+
startAt(): Query;
|
|
16
|
+
withConverter(): Query;
|
|
17
|
+
onSnapshot(): () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const mocks: {
|
|
21
|
+
mockGet: Mock<any>,
|
|
22
|
+
mockSelect: Mock<any>,
|
|
23
|
+
mockWhere: Mock<any>,
|
|
24
|
+
mockLimit: Mock<any>,
|
|
25
|
+
mockOrderBy: Mock<any>,
|
|
26
|
+
mockOffset: Mock<any>,
|
|
27
|
+
mockStartAfter: Mock<any>,
|
|
28
|
+
mockStartAt: Mock<any>,
|
|
29
|
+
mockQueryOnSnapshot: Mock<any>,
|
|
30
|
+
mockQueryOnSnapshotUnsubscribe: Mock<any>,
|
|
31
|
+
mockWithConverter: Mock<any>,
|
|
32
|
+
};
|
package/mocks/query.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { mock } from 'node:test';
|
|
2
|
+
import buildQuerySnapShot from './helpers/buildQuerySnapShot.js';
|
|
3
|
+
|
|
4
|
+
export const mockGet = mock.fn();
|
|
5
|
+
export const mockSelect = mock.fn();
|
|
6
|
+
export const mockWhere = mock.fn();
|
|
7
|
+
export const mockLimit = mock.fn();
|
|
8
|
+
export const mockOrderBy = mock.fn();
|
|
9
|
+
export const mockOffset = mock.fn();
|
|
10
|
+
export const mockStartAfter = mock.fn();
|
|
11
|
+
export const mockStartAt = mock.fn();
|
|
12
|
+
export const mockQueryOnSnapshot = mock.fn();
|
|
13
|
+
export const mockQueryOnSnapshotUnsubscribe = mock.fn();
|
|
14
|
+
export const mockWithConverter = mock.fn();
|
|
15
|
+
|
|
16
|
+
export class Query {
|
|
17
|
+
constructor(collectionName, firestore, isGroupQuery = false) {
|
|
18
|
+
this.collectionName = collectionName;
|
|
19
|
+
this.firestore = firestore;
|
|
20
|
+
this.filters = [];
|
|
21
|
+
this.selectFields = undefined;
|
|
22
|
+
this.isGroupQuery = isGroupQuery;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get() {
|
|
26
|
+
mockGet(...arguments);
|
|
27
|
+
return Promise.resolve(this._get());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_get() {
|
|
31
|
+
// Simulate collectionGroup query
|
|
32
|
+
|
|
33
|
+
// Get Firestore collections whose name match `this.collectionName`; return their documents
|
|
34
|
+
const requestedRecords = [];
|
|
35
|
+
|
|
36
|
+
const queue = [
|
|
37
|
+
{
|
|
38
|
+
lastParent: '',
|
|
39
|
+
collections: this.firestore.database,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
while (queue.length > 0) {
|
|
44
|
+
// Get a collection
|
|
45
|
+
const { lastParent, collections } = queue.shift();
|
|
46
|
+
|
|
47
|
+
Object.entries(collections).forEach(([collectionPath, docs]) => {
|
|
48
|
+
const prefix = lastParent ? `${lastParent}/` : '';
|
|
49
|
+
|
|
50
|
+
const newLastParent = `${prefix}${collectionPath}`;
|
|
51
|
+
const lastPathComponent = collectionPath.split('/').pop();
|
|
52
|
+
|
|
53
|
+
// If this is a matching collection, grep its documents
|
|
54
|
+
if (lastPathComponent === this.collectionName) {
|
|
55
|
+
const docHashes = docs.map(doc => {
|
|
56
|
+
// Fetch the document from the mock db
|
|
57
|
+
const path = `${newLastParent}/${doc.id}`;
|
|
58
|
+
return {
|
|
59
|
+
...doc,
|
|
60
|
+
_ref: this.firestore._doc(path),
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
requestedRecords.push(...docHashes);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Enqueue adjacent collections for next run
|
|
67
|
+
docs.forEach(doc => {
|
|
68
|
+
if (doc._collections) {
|
|
69
|
+
queue.push({
|
|
70
|
+
lastParent: `${prefix}${collectionPath}/${doc.id}`,
|
|
71
|
+
collections: doc._collections,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Return the requested documents
|
|
79
|
+
const isFilteringEnabled = this.firestore.options.simulateQueryFilters;
|
|
80
|
+
return buildQuerySnapShot(
|
|
81
|
+
requestedRecords,
|
|
82
|
+
isFilteringEnabled ? this.filters : undefined,
|
|
83
|
+
this.selectFields,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
select(...fieldPaths) {
|
|
88
|
+
this.selectFields = fieldPaths;
|
|
89
|
+
return mockSelect(...fieldPaths) || this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
where(key, comp, value) {
|
|
93
|
+
const result = mockWhere(...arguments);
|
|
94
|
+
if (result) {
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Firestore has been tested to throw an error at this point when trying to compare null as a quantity
|
|
99
|
+
if (value === null && !['==', '!='].includes(comp)) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`FakeFirebaseError: Invalid query. Null only supports '==' and '!=' comparisons.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
this.filters.push({ key, comp, value });
|
|
105
|
+
return result || this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
offset() {
|
|
109
|
+
return mockOffset(...arguments) || this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
limit() {
|
|
113
|
+
return mockLimit(...arguments) || this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
orderBy() {
|
|
117
|
+
return mockOrderBy(...arguments) || this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
startAfter() {
|
|
121
|
+
return mockStartAfter(...arguments) || this;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
startAt() {
|
|
125
|
+
return mockStartAt(...arguments) || this;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
withConverter() {
|
|
129
|
+
return mockWithConverter(...arguments) || this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
onSnapshot() {
|
|
133
|
+
mockQueryOnSnapshot(...arguments);
|
|
134
|
+
const [callback, errorCallback] = arguments;
|
|
135
|
+
try {
|
|
136
|
+
callback(this._get());
|
|
137
|
+
} catch (e) {
|
|
138
|
+
if (errorCallback) {
|
|
139
|
+
errorCallback(e);
|
|
140
|
+
} else {
|
|
141
|
+
throw e;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Returns an unsubscribe mock
|
|
146
|
+
return mockQueryOnSnapshotUnsubscribe;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const mocks = {
|
|
151
|
+
mockGet,
|
|
152
|
+
mockSelect,
|
|
153
|
+
mockWhere,
|
|
154
|
+
mockLimit,
|
|
155
|
+
mockOrderBy,
|
|
156
|
+
mockOffset,
|
|
157
|
+
mockStartAfter,
|
|
158
|
+
mockStartAt,
|
|
159
|
+
mockQueryOnSnapshot,
|
|
160
|
+
mockQueryOnSnapshotUnsubscribe,
|
|
161
|
+
mockWithConverter,
|
|
162
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { StubOverrides, StubOptions } from './firebase.js';
|
|
2
|
+
import type { FakeFirestore } from './firestore.js';
|
|
3
|
+
|
|
4
|
+
declare class Firestore extends FakeFirestore {
|
|
5
|
+
constructor();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface GCloudFirestoreMock {
|
|
9
|
+
Firestore: typeof Firestore;
|
|
10
|
+
Query: typeof Firestore.Query;
|
|
11
|
+
CollectionReference: typeof Firestore.CollectionReference;
|
|
12
|
+
DocumentReference: typeof Firestore.DocumentReference;
|
|
13
|
+
FieldValue: typeof Firestore.FieldValue;
|
|
14
|
+
FieldPath: typeof Firestore.FieldPath;
|
|
15
|
+
Timestamp: typeof Firestore.Timestamp;
|
|
16
|
+
Transaction: typeof Firestore.Transaction;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const firestoreStub: (overrides: StubOverrides, options?: StubOptions) => GCloudFirestoreMock;
|
|
20
|
+
export const mockReactNativeFirestore: (overrides: StubOverrides, options?: StubOptions) => void;
|