@aglyn/tenant-data-admin 1.0.0-beta.196 → 1.0.0-beta.198

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.
@@ -0,0 +1,640 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /**
18
+ * A Firestore stand-in that QUERIES (AGL-3320). Test-only; deliberately not
19
+ * exported from the barrel, like `test-firestore.ts` beside it.
20
+ *
21
+ * That fake holds documents and nothing else, which is enough for code that
22
+ * reads a document by id. A consent group change pages whole subcollections
23
+ * by document id, filters on nested map fields, counts, writes through a
24
+ * `BulkWriter` with `lastUpdateTime` preconditions and takes transactions —
25
+ * and its correctness properties (a second pass writes nothing, a resumed
26
+ * pass skips nothing) are properties of exactly those mechanics, so a fake
27
+ * that skipped them would be asserting itself.
28
+ *
29
+ * What is modeled, and is load-bearing:
30
+ *
31
+ * - A document missing a field never matches a filter or an ordering on it,
32
+ * including `!=` — the rule that makes an `orderBy` on a data field drop
33
+ * documents, and the reason every scan here pages by `__name__`.
34
+ * - `update()` needs the document to exist, replaces the value at each
35
+ * (dotted) path whole, and fails its `lastUpdateTime` precondition when
36
+ * the document was written since; `create()` fails on an existing one.
37
+ * The error codes are gRPC's: 5, 6, 9.
38
+ * - `FieldValue` sentinels are applied, not stored: `delete`,
39
+ * `serverTimestamp` (the fake clock), `increment`, `arrayUnion`,
40
+ * `arrayRemove`.
41
+ * - Every write moves `updateTime`, and a `BulkWriter` runs its queued
42
+ * writes when it is flushed or closed, rejecting each failed write's
43
+ * promise the way the real one does.
44
+ * - A transaction is SERIALIZABLE: one whose reads changed before it
45
+ * committed runs its body again, as the SDK retries a contended one, so
46
+ * two transactions racing on one document cannot both act on what they
47
+ * read. That is the property a start's "no change in flight" rests on.
48
+ *
49
+ * What is not: a `BulkWriter`'s own retries, which nothing here branches on.
50
+ */ import { FieldValue, Timestamp } from "firebase-admin/firestore";
51
+ /** A Firestore-style error: the gRPC code the SDK attaches. */ function grpcError(code, message) {
52
+ return Object.assign(new Error(message), {
53
+ code
54
+ });
55
+ }
56
+ const isTransform = (value)=>value instanceof FieldValue;
57
+ function clone(value) {
58
+ if (value === null || typeof value !== 'object') return value;
59
+ if (value instanceof Timestamp || value instanceof FieldValue) return value;
60
+ if (Array.isArray(value)) return value.map((item)=>clone(item));
61
+ const out = {};
62
+ for (const [key, item] of Object.entries(value))out[key] = clone(item);
63
+ return out;
64
+ }
65
+ /** A field as a plain dotted path; a `FieldPath`'s segments are joined unquoted. */ function fieldName(field) {
66
+ if (typeof field === 'string') return field;
67
+ const segments = field.segments;
68
+ return Array.isArray(segments) ? segments.join('.') : String(field);
69
+ }
70
+ /** The value at a dotted path, or `undefined` when any segment is missing. */ function valueAt(data, path) {
71
+ let node = data;
72
+ for (const segment of path.split('.')){
73
+ if (!node || typeof node !== 'object' || Array.isArray(node)) return undefined;
74
+ node = node[segment];
75
+ }
76
+ return node;
77
+ }
78
+ /** Firestore's cross-type order: null, booleans, numbers, timestamps, strings, the rest. */ function rank(value) {
79
+ if (value === null) return 0;
80
+ if (typeof value === 'boolean') return 1;
81
+ if (typeof value === 'number') return 2;
82
+ if (value instanceof Timestamp) return 3;
83
+ if (typeof value === 'string') return 4;
84
+ if (Array.isArray(value)) return 6;
85
+ return 7;
86
+ }
87
+ function compare(a, b) {
88
+ const byRank = rank(a) - rank(b);
89
+ if (byRank !== 0) return byRank;
90
+ if (a instanceof Timestamp && b instanceof Timestamp) {
91
+ return a.seconds - b.seconds || a.nanoseconds - b.nanoseconds;
92
+ }
93
+ if (typeof a === 'number' && typeof b === 'number') return a - b;
94
+ if (typeof a === 'string' && typeof b === 'string') return a < b ? -1 : a > b ? 1 : 0;
95
+ if (typeof a === 'boolean' && typeof b === 'boolean') return Number(a) - Number(b);
96
+ const left = JSON.stringify(a);
97
+ const right = JSON.stringify(b);
98
+ return left < right ? -1 : left > right ? 1 : 0;
99
+ }
100
+ const equal = (a, b)=>rank(a) === rank(b) && compare(a, b) === 0;
101
+ function matches(id, data, filter) {
102
+ const actual = filter.field === '__name__' ? id : valueAt(data, filter.field);
103
+ if (actual === undefined) return false;
104
+ const { op, value } = filter;
105
+ switch(op){
106
+ case '==':
107
+ return equal(actual, value);
108
+ case '!=':
109
+ return actual !== null && !equal(actual, value);
110
+ case '<':
111
+ return rank(actual) === rank(value) && compare(actual, value) < 0;
112
+ case '<=':
113
+ return rank(actual) === rank(value) && compare(actual, value) <= 0;
114
+ case '>':
115
+ return rank(actual) === rank(value) && compare(actual, value) > 0;
116
+ case '>=':
117
+ return rank(actual) === rank(value) && compare(actual, value) >= 0;
118
+ case 'in':
119
+ return value.some((candidate)=>equal(actual, candidate));
120
+ case 'not-in':
121
+ return actual !== null && !value.some((candidate)=>equal(actual, candidate));
122
+ case 'array-contains':
123
+ return Array.isArray(actual) && actual.some((item)=>equal(item, value));
124
+ case 'array-contains-any':
125
+ return Array.isArray(actual) && actual.some((item)=>value.some((candidate)=>equal(item, candidate)));
126
+ default:
127
+ throw new Error(`test-firestore-queries: unsupported operator ${op}`);
128
+ }
129
+ }
130
+ export function queryFakeFirestore(seed = {}, options = {}) {
131
+ var _options_nowMs;
132
+ const store = new Map();
133
+ let nowMs = (_options_nowMs = options.nowMs) != null ? _options_nowMs : Date.UTC(2026, 8, 24);
134
+ /** Keeps every write's `updateTime` distinct even on a frozen clock. */ let tick = 0;
135
+ let writeCount = 0;
136
+ const failing = new Map();
137
+ let autoIds = 0;
138
+ const stamp = ()=>Timestamp.fromMillis(nowMs + (tick += 0.001));
139
+ const nextAutoId = ()=>{
140
+ autoIds += 1;
141
+ return String(autoIds).padStart(8, '0');
142
+ };
143
+ for (const [path, data] of Object.entries(seed)){
144
+ const at = Timestamp.fromMillis(nowMs);
145
+ store.set(path, {
146
+ data: clone(data),
147
+ createTime: at,
148
+ updateTime: at
149
+ });
150
+ }
151
+ /** Applies a write's value at one path of `target`, honoring transforms. */ function put(target, path, value, previous) {
152
+ const [head, ...rest] = path;
153
+ if (rest.length) {
154
+ const child = target[head];
155
+ const next = child && typeof child === 'object' && !Array.isArray(child) ? child : {};
156
+ target[head] = next;
157
+ put(next, rest, value, valueAt(previous, rest.join('.')));
158
+ return;
159
+ }
160
+ if (isTransform(value)) {
161
+ const current = target[head];
162
+ switch(value.methodName){
163
+ case 'FieldValue.delete':
164
+ delete target[head];
165
+ return;
166
+ case 'FieldValue.serverTimestamp':
167
+ target[head] = Timestamp.fromMillis(nowMs);
168
+ return;
169
+ case 'FieldValue.increment':
170
+ {
171
+ const operand = value.operand;
172
+ target[head] = (typeof current === 'number' ? current : 0) + operand;
173
+ return;
174
+ }
175
+ case 'FieldValue.arrayUnion':
176
+ {
177
+ const elements = value.elements;
178
+ const base = Array.isArray(current) ? [
179
+ ...current
180
+ ] : [];
181
+ for (const element of elements){
182
+ if (!base.some((item)=>equal(item, element))) base.push(element);
183
+ }
184
+ target[head] = base;
185
+ return;
186
+ }
187
+ case 'FieldValue.arrayRemove':
188
+ {
189
+ const elements = value.elements;
190
+ target[head] = (Array.isArray(current) ? current : []).filter((item)=>!elements.some((element)=>equal(item, element)));
191
+ return;
192
+ }
193
+ default:
194
+ throw new Error(`test-firestore-queries: unsupported transform ${value.methodName}`);
195
+ }
196
+ }
197
+ target[head] = materialize(value);
198
+ }
199
+ /** A written value with any nested transforms resolved against nothing. */ function materialize(value) {
200
+ if (value === undefined) throw new Error('test-firestore-queries: undefined is not a Firestore value');
201
+ if (value === null || typeof value !== 'object') return value;
202
+ if (value instanceof Timestamp) return value;
203
+ if (isTransform(value)) {
204
+ if (value.methodName === 'FieldValue.serverTimestamp') return Timestamp.fromMillis(nowMs);
205
+ throw new Error(`test-firestore-queries: ${value.methodName} inside a nested value`);
206
+ }
207
+ if (Array.isArray(value)) return value.map((item)=>materialize(item));
208
+ const out = {};
209
+ for (const [key, item] of Object.entries(value)){
210
+ if (isTransform(item) && item.methodName === 'FieldValue.delete') continue;
211
+ out[key] = materialize(item);
212
+ }
213
+ return out;
214
+ }
215
+ function mergeInto(target, source) {
216
+ for (const [key, value] of Object.entries(source)){
217
+ if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Timestamp) && !isTransform(value)) {
218
+ const child = target[key];
219
+ const next = child && typeof child === 'object' && !Array.isArray(child) && !(child instanceof Timestamp) ? child : {};
220
+ target[key] = next;
221
+ mergeInto(next, value);
222
+ } else {
223
+ put(target, [
224
+ key
225
+ ], value, target[key]);
226
+ }
227
+ }
228
+ }
229
+ /** Why a write cannot apply, or `null`. */ function refusal(write) {
230
+ var _write_precondition;
231
+ const failure = failing.get(write.path);
232
+ if (failure) return failure;
233
+ const existing = store.get(write.path);
234
+ if (write.type === 'create' && existing) {
235
+ return grpcError(6, `ALREADY_EXISTS: ${write.path}`);
236
+ }
237
+ if (write.type === 'update' && !existing) {
238
+ return grpcError(5, `NOT_FOUND: ${write.path}`);
239
+ }
240
+ if ((write.type === 'update' || write.type === 'delete') && ((_write_precondition = write.precondition) == null ? void 0 : _write_precondition.lastUpdateTime)) {
241
+ if (!existing || !existing.updateTime.isEqual(write.precondition.lastUpdateTime)) {
242
+ return grpcError(9, `FAILED_PRECONDITION: ${write.path}`);
243
+ }
244
+ }
245
+ return null;
246
+ }
247
+ function apply(write) {
248
+ var _ref;
249
+ const existing = store.get(write.path);
250
+ const at = stamp();
251
+ writeCount += 1;
252
+ if (write.type === 'delete') {
253
+ store.delete(write.path);
254
+ return at;
255
+ }
256
+ if (write.type === 'update') {
257
+ var _ref1, _ref2;
258
+ const data = clone((_ref1 = existing == null ? void 0 : existing.data) != null ? _ref1 : {});
259
+ for (const [path, value] of Object.entries(write.data)){
260
+ put(data, path.split('.'), value, valueAt(existing == null ? void 0 : existing.data, path));
261
+ }
262
+ store.set(write.path, {
263
+ data,
264
+ createTime: (_ref2 = existing == null ? void 0 : existing.createTime) != null ? _ref2 : at,
265
+ updateTime: at
266
+ });
267
+ return at;
268
+ }
269
+ if (write.type === 'set' && write.merge) {
270
+ var _ref3, _ref4;
271
+ const data = clone((_ref3 = existing == null ? void 0 : existing.data) != null ? _ref3 : {});
272
+ mergeInto(data, write.data);
273
+ store.set(write.path, {
274
+ data,
275
+ createTime: (_ref4 = existing == null ? void 0 : existing.createTime) != null ? _ref4 : at,
276
+ updateTime: at
277
+ });
278
+ return at;
279
+ }
280
+ const data = {};
281
+ mergeInto(data, write.data);
282
+ store.set(write.path, {
283
+ data,
284
+ createTime: (_ref = existing == null ? void 0 : existing.createTime) != null ? _ref : at,
285
+ updateTime: at
286
+ });
287
+ return at;
288
+ }
289
+ function snapshot(path) {
290
+ const stored = store.get(path);
291
+ const id = path.split('/').pop();
292
+ return {
293
+ id,
294
+ ref: docRef(path),
295
+ exists: stored !== undefined,
296
+ data: ()=>stored ? clone(stored.data) : undefined,
297
+ get: (field)=>clone(valueAt(stored == null ? void 0 : stored.data, fieldName(field))),
298
+ updateTime: stored == null ? void 0 : stored.updateTime,
299
+ createTime: stored == null ? void 0 : stored.createTime
300
+ };
301
+ }
302
+ function docRef(path) {
303
+ const segments = path.split('/');
304
+ if (segments.length % 2 !== 0) throw new Error(`test-firestore-queries: ${path} is not a document path`);
305
+ const ref = {
306
+ id: segments[segments.length - 1],
307
+ path,
308
+ collection: (name)=>collectionRef(`${path}/${name}`),
309
+ get: async ()=>snapshot(path),
310
+ set: async (data, setOptions)=>run({
311
+ type: 'set',
312
+ path,
313
+ data,
314
+ merge: (setOptions == null ? void 0 : setOptions.merge) === true
315
+ }),
316
+ update: async (data, precondition)=>run({
317
+ type: 'update',
318
+ path,
319
+ data,
320
+ precondition
321
+ }),
322
+ create: async (data)=>run({
323
+ type: 'create',
324
+ path,
325
+ data
326
+ }),
327
+ delete: async (precondition)=>run({
328
+ type: 'delete',
329
+ path,
330
+ precondition
331
+ }),
332
+ isEqual: (other)=>(other == null ? void 0 : other.path) === path
333
+ };
334
+ Object.defineProperty(ref, 'parent', {
335
+ get: ()=>collectionRef(segments.slice(0, -1).join('/'))
336
+ });
337
+ return ref;
338
+ }
339
+ function run(write) {
340
+ const failure = refusal(write);
341
+ if (failure) throw failure;
342
+ return {
343
+ writeTime: apply(write)
344
+ };
345
+ }
346
+ function execute(state) {
347
+ const prefix = `${state.path}/`;
348
+ let rows = [
349
+ ...store.entries()
350
+ ].filter(([path])=>path.startsWith(prefix) && !path.slice(prefix.length).includes('/')).map(([path, stored])=>({
351
+ path,
352
+ id: path.slice(prefix.length),
353
+ data: stored.data
354
+ })).filter((row)=>state.filters.every((filter)=>matches(row.id, row.data, filter)));
355
+ const orders = [
356
+ ...state.orders
357
+ ];
358
+ if (!orders.some((order)=>order.field === '__name__')) {
359
+ orders.push({
360
+ field: '__name__',
361
+ direction: 'asc'
362
+ });
363
+ }
364
+ rows = rows.filter((row)=>orders.every((order)=>order.field === '__name__' || valueAt(row.data, order.field) !== undefined));
365
+ const keyOf = (row, field)=>field === '__name__' ? row.id : valueAt(row.data, field);
366
+ rows.sort((a, b)=>{
367
+ for (const order of orders){
368
+ const result = compare(keyOf(a, order.field), keyOf(b, order.field));
369
+ if (result !== 0) return order.direction === 'desc' ? -result : result;
370
+ }
371
+ return 0;
372
+ });
373
+ if (state.after) {
374
+ const cursor = state.after;
375
+ rows = rows.filter((row)=>{
376
+ for(let index = 0; index < cursor.length; index += 1){
377
+ const order = orders[index];
378
+ const result = compare(keyOf(row, order.field), cursor[index]);
379
+ const signed = order.direction === 'desc' ? -result : result;
380
+ if (signed !== 0) return signed > 0;
381
+ }
382
+ return false;
383
+ });
384
+ }
385
+ if (state.limit !== null) rows = rows.slice(0, state.limit);
386
+ return rows.map((row)=>snapshot(row.path));
387
+ }
388
+ function query(state) {
389
+ return {
390
+ where: (field, op, value)=>query(_extends({}, state, {
391
+ filters: [
392
+ ...state.filters,
393
+ {
394
+ field: fieldName(field),
395
+ op,
396
+ value
397
+ }
398
+ ]
399
+ })),
400
+ orderBy: (field, direction = 'asc')=>query(_extends({}, state, {
401
+ orders: [
402
+ ...state.orders,
403
+ {
404
+ field: fieldName(field),
405
+ direction
406
+ }
407
+ ]
408
+ })),
409
+ startAfter: (...values)=>query(_extends({}, state, {
410
+ after: values
411
+ })),
412
+ limit: (limit)=>query(_extends({}, state, {
413
+ limit
414
+ })),
415
+ count: ()=>({
416
+ get: async ()=>{
417
+ const total = execute(_extends({}, state, {
418
+ limit: null,
419
+ after: state.after
420
+ })).length;
421
+ const count = state.limit === null ? total : Math.min(total, state.limit);
422
+ return {
423
+ data: ()=>({
424
+ count
425
+ })
426
+ };
427
+ }
428
+ }),
429
+ get: async ()=>{
430
+ const docs = execute(state);
431
+ return {
432
+ docs,
433
+ size: docs.length,
434
+ empty: docs.length === 0,
435
+ forEach: (fn)=>docs.forEach(fn)
436
+ };
437
+ }
438
+ };
439
+ }
440
+ function collectionRef(path) {
441
+ const base = query({
442
+ path,
443
+ filters: [],
444
+ orders: [],
445
+ after: null,
446
+ limit: null
447
+ });
448
+ return _extends({}, base, {
449
+ id: path.split('/').pop(),
450
+ path,
451
+ doc: (id)=>docRef(`${path}/${id != null ? id : nextAutoId()}`),
452
+ add: async (data)=>{
453
+ const ref = docRef(`${path}/${nextAutoId()}`);
454
+ run({
455
+ type: 'create',
456
+ path: ref.path,
457
+ data
458
+ });
459
+ return ref;
460
+ }
461
+ });
462
+ }
463
+ function transactionOrBatch() {
464
+ const writes = [];
465
+ const reads = new Map();
466
+ const read = (path)=>{
467
+ var _ref;
468
+ var _store_get;
469
+ if (!reads.has(path)) reads.set(path, (_ref = (_store_get = store.get(path)) == null ? void 0 : _store_get.updateTime) != null ? _ref : null);
470
+ return snapshot(path);
471
+ };
472
+ const api = {
473
+ get: async (target)=>typeof (target == null ? void 0 : target.path) === 'string' && target.path.split('/').length % 2 === 0 ? read(target.path) : target.get(),
474
+ getAll: async (...refs)=>refs.map((ref)=>read(ref.path)),
475
+ set: (ref, data, setOptions)=>{
476
+ writes.push({
477
+ type: 'set',
478
+ path: ref.path,
479
+ data,
480
+ merge: (setOptions == null ? void 0 : setOptions.merge) === true
481
+ });
482
+ return api;
483
+ },
484
+ update: (ref, data, precondition)=>{
485
+ writes.push({
486
+ type: 'update',
487
+ path: ref.path,
488
+ data,
489
+ precondition
490
+ });
491
+ return api;
492
+ },
493
+ create: (ref, data)=>{
494
+ writes.push({
495
+ type: 'create',
496
+ path: ref.path,
497
+ data
498
+ });
499
+ return api;
500
+ },
501
+ delete: (ref, precondition)=>{
502
+ writes.push({
503
+ type: 'delete',
504
+ path: ref.path,
505
+ precondition
506
+ });
507
+ return api;
508
+ }
509
+ };
510
+ return {
511
+ writes,
512
+ reads,
513
+ api
514
+ };
515
+ }
516
+ /** Whether every document a transaction read is still as it read it. */ function unchanged(reads) {
517
+ for (const [path, seen] of reads){
518
+ var _ref;
519
+ var _store_get;
520
+ const now = (_ref = (_store_get = store.get(path)) == null ? void 0 : _store_get.updateTime) != null ? _ref : null;
521
+ if (seen === null ? now !== null : now === null || !now.isEqual(seen)) return false;
522
+ }
523
+ return true;
524
+ }
525
+ /** All or nothing, in order: every write is checked against the state the earlier ones leave. */ function commitAll(writes) {
526
+ const saved = new Map(store);
527
+ const count = writeCount;
528
+ try {
529
+ for (const write of writes)run(write);
530
+ } catch (error) {
531
+ store.clear();
532
+ for (const [path, stored] of saved)store.set(path, stored);
533
+ writeCount = count;
534
+ throw error;
535
+ }
536
+ }
537
+ const firestore = {
538
+ collection: (path)=>collectionRef(path),
539
+ doc: (path)=>docRef(path),
540
+ getAll: async (...refs)=>refs.map((ref)=>snapshot(ref.path)),
541
+ runTransaction: async (body)=>{
542
+ for(let attempt = 0; attempt < 5; attempt += 1){
543
+ const { writes, reads, api } = transactionOrBatch();
544
+ const result = await body(api);
545
+ if (!unchanged(reads)) continue;
546
+ commitAll(writes);
547
+ return result;
548
+ }
549
+ throw grpcError(10, 'ABORTED: too much contention');
550
+ },
551
+ batch: ()=>{
552
+ const { writes, api } = transactionOrBatch();
553
+ return _extends({}, api, {
554
+ commit: async ()=>commitAll(writes)
555
+ });
556
+ },
557
+ bulkWriter: ()=>{
558
+ const queued = [];
559
+ const enqueue = (write)=>new Promise((resolve, reject)=>queued.push({
560
+ write,
561
+ resolve,
562
+ reject
563
+ }));
564
+ const drain = async ()=>{
565
+ while(queued.length){
566
+ const next = queued.shift();
567
+ try {
568
+ next.resolve(run(next.write));
569
+ } catch (error) {
570
+ next.reject(error);
571
+ }
572
+ }
573
+ };
574
+ return {
575
+ create: (ref, data)=>enqueue({
576
+ type: 'create',
577
+ path: ref.path,
578
+ data
579
+ }),
580
+ set: (ref, data, setOptions)=>enqueue({
581
+ type: 'set',
582
+ path: ref.path,
583
+ data,
584
+ merge: (setOptions == null ? void 0 : setOptions.merge) === true
585
+ }),
586
+ update: (ref, data, precondition)=>enqueue({
587
+ type: 'update',
588
+ path: ref.path,
589
+ data,
590
+ precondition
591
+ }),
592
+ delete: (ref, precondition)=>enqueue({
593
+ type: 'delete',
594
+ path: ref.path,
595
+ precondition
596
+ }),
597
+ onWriteError: ()=>undefined,
598
+ flush: drain,
599
+ close: drain
600
+ };
601
+ },
602
+ seed: (path, data)=>{
603
+ var _ref;
604
+ const at = Timestamp.fromMillis(nowMs);
605
+ const existing = store.get(path);
606
+ store.set(path, {
607
+ data: clone(data),
608
+ createTime: (_ref = existing == null ? void 0 : existing.createTime) != null ? _ref : at,
609
+ updateTime: stamp()
610
+ });
611
+ },
612
+ read: (path)=>{
613
+ const stored = store.get(path);
614
+ return stored ? clone(stored.data) : undefined;
615
+ },
616
+ docs: (collectionPath)=>{
617
+ const prefix = `${collectionPath}/`;
618
+ const out = {};
619
+ for (const [path, stored] of store){
620
+ const rest = path.slice(prefix.length);
621
+ if (path.startsWith(prefix) && !rest.includes('/')) out[rest] = clone(stored.data);
622
+ }
623
+ return out;
624
+ },
625
+ writes: ()=>writeCount,
626
+ resetWrites: ()=>{
627
+ writeCount = 0;
628
+ },
629
+ setNow: (ms)=>{
630
+ nowMs = ms;
631
+ },
632
+ failWritesTo: (path, error)=>{
633
+ if (error) failing.set(path, error);
634
+ else failing.delete(path);
635
+ }
636
+ };
637
+ return firestore;
638
+ }
639
+
640
+ //# sourceMappingURL=test-firestore-queries.js.map