@naturalcycles/firestore-lib 2.14.4 → 2.16.0
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/dist/firestore.db.d.ts +3 -3
- package/dist/firestore.db.js +43 -14
- package/dist/firestoreStreamReadable.d.ts +1 -0
- package/dist/firestoreStreamReadable.js +9 -1
- package/dist/query.util.d.ts +3 -1
- package/dist/query.util.js +16 -2
- package/package.json +4 -5
- package/src/firestore.db.ts +47 -14
- package/src/firestoreStreamReadable.ts +15 -2
- package/src/index.ts +1 -0
- package/src/query.util.ts +15 -2
package/dist/firestore.db.d.ts
CHANGED
|
@@ -12,9 +12,9 @@ export declare class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
12
12
|
support: CommonDBSupport;
|
|
13
13
|
getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: FirestoreDBReadOptions): Promise<ROW[]>;
|
|
14
14
|
multiGet<ROW extends ObjectWithId>(map: StringMap<string[]>, opt?: CommonDBReadOptions): Promise<StringMap<ROW[]>>;
|
|
15
|
-
runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?:
|
|
16
|
-
runFirestoreQuery<ROW extends ObjectWithId>(q: Query): Promise<ROW[]>;
|
|
17
|
-
runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>,
|
|
15
|
+
runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: FirestoreDBReadOptions): Promise<RunQueryResult<ROW>>;
|
|
16
|
+
runFirestoreQuery<ROW extends ObjectWithId>(q: Query, opt?: FirestoreDBReadOptions): Promise<ROW[]>;
|
|
17
|
+
runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: FirestoreDBReadOptions): Promise<number>;
|
|
18
18
|
streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt_?: FirestoreDBStreamOptions): Pipeline<ROW>;
|
|
19
19
|
saveBatch<ROW extends ObjectWithId>(table: string, rows: ROW[], opt?: FirestoreDBSaveOptions<ROW>): Promise<void>;
|
|
20
20
|
multiSave<ROW extends ObjectWithId>(map: StringMap<ROW[]>, opt?: FirestoreDBSaveOptions<ROW>): Promise<void>;
|
package/dist/firestore.db.js
CHANGED
|
@@ -10,7 +10,7 @@ import { Pipeline } from '@naturalcycles/nodejs-lib/stream';
|
|
|
10
10
|
import { escapeDocId, unescapeDocId } from './firestore.util.js';
|
|
11
11
|
import { FirestoreShardedReadable } from './firestoreShardedReadable.js';
|
|
12
12
|
import { FirestoreStreamReadable } from './firestoreStreamReadable.js';
|
|
13
|
-
import { dbQueryToFirestoreQuery } from './query.util.js';
|
|
13
|
+
import { dbQueryToFirestoreQuery, readAtToReadTime } from './query.util.js';
|
|
14
14
|
export class FirestoreDB extends BaseCommonDB {
|
|
15
15
|
constructor(cfg) {
|
|
16
16
|
super();
|
|
@@ -30,10 +30,17 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
30
30
|
async getByIds(table, ids, opt = {}) {
|
|
31
31
|
if (!ids.length)
|
|
32
32
|
return [];
|
|
33
|
-
// todo: support PITR: https://firebase.google.com/docs/firestore/enterprise/use-pitr#read-pitr
|
|
34
33
|
const { firestore } = this.cfg;
|
|
35
34
|
const col = firestore.collection(table);
|
|
36
|
-
|
|
35
|
+
const refs = ids.map(id => col.doc(escapeDocId(id)));
|
|
36
|
+
const readTime = readAtToReadTime(opt);
|
|
37
|
+
if (readTime) {
|
|
38
|
+
_assert(!opt.tx, 'readAt is not supported inside an existing transaction');
|
|
39
|
+
}
|
|
40
|
+
const snapshots = await (readTime
|
|
41
|
+
? firestore.runTransaction(tx => tx.getAll(...refs), { readOnly: true, readTime })
|
|
42
|
+
: (opt.tx?.tx || firestore).getAll(...refs));
|
|
43
|
+
return snapshots
|
|
37
44
|
.map(doc => {
|
|
38
45
|
const data = doc.data();
|
|
39
46
|
if (data === undefined)
|
|
@@ -54,7 +61,13 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
54
61
|
const col = firestore.collection(table);
|
|
55
62
|
refs.push(...ids.map(id => col.doc(escapeDocId(id))));
|
|
56
63
|
}
|
|
57
|
-
const
|
|
64
|
+
const readTime = readAtToReadTime(opt);
|
|
65
|
+
if (readTime) {
|
|
66
|
+
_assert(!opt.tx, 'readAt is not supported inside an existing transaction');
|
|
67
|
+
}
|
|
68
|
+
const snapshots = await (readTime
|
|
69
|
+
? firestore.runTransaction(tx => tx.getAll(...refs), { readOnly: true, readTime })
|
|
70
|
+
: (opt.tx?.tx || firestore).getAll(...refs));
|
|
58
71
|
snapshots.forEach(snap => {
|
|
59
72
|
const data = snap.data();
|
|
60
73
|
if (data === undefined)
|
|
@@ -69,7 +82,7 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
69
82
|
return result;
|
|
70
83
|
}
|
|
71
84
|
// QUERY
|
|
72
|
-
async runQuery(q, opt) {
|
|
85
|
+
async runQuery(q, opt = {}) {
|
|
73
86
|
const idFilter = q._filters.find(f => f.name === 'id');
|
|
74
87
|
if (idFilter) {
|
|
75
88
|
const ids = Array.isArray(idFilter.val) ? idFilter.val : [idFilter.val];
|
|
@@ -78,18 +91,34 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
78
91
|
};
|
|
79
92
|
}
|
|
80
93
|
const firestoreQuery = dbQueryToFirestoreQuery(q, this.cfg.firestore.collection(q.table));
|
|
81
|
-
let rows = await this.runFirestoreQuery(firestoreQuery);
|
|
94
|
+
let rows = await this.runFirestoreQuery(firestoreQuery, opt);
|
|
82
95
|
// Special case when projection query didn't specify 'id'
|
|
83
96
|
if (q._selectedFieldNames && !q._selectedFieldNames.includes('id')) {
|
|
84
97
|
rows = rows.map(r => _omit(r, ['id']));
|
|
85
98
|
}
|
|
86
99
|
return { rows };
|
|
87
100
|
}
|
|
88
|
-
async runFirestoreQuery(q) {
|
|
101
|
+
async runFirestoreQuery(q, opt = {}) {
|
|
102
|
+
const readTime = readAtToReadTime(opt);
|
|
103
|
+
if (readTime) {
|
|
104
|
+
const qs = await this.cfg.firestore.runTransaction(tx => tx.get(q), {
|
|
105
|
+
readOnly: true,
|
|
106
|
+
readTime,
|
|
107
|
+
});
|
|
108
|
+
return this.querySnapshotToArray(qs);
|
|
109
|
+
}
|
|
89
110
|
return this.querySnapshotToArray(await q.get());
|
|
90
111
|
}
|
|
91
|
-
async runQueryCount(q,
|
|
112
|
+
async runQueryCount(q, opt = {}) {
|
|
92
113
|
const firestoreQuery = dbQueryToFirestoreQuery(q, this.cfg.firestore.collection(q.table));
|
|
114
|
+
const readTime = readAtToReadTime(opt);
|
|
115
|
+
if (readTime) {
|
|
116
|
+
const r = await this.cfg.firestore.runTransaction(tx => tx.get(firestoreQuery.count()), {
|
|
117
|
+
readOnly: true,
|
|
118
|
+
readTime,
|
|
119
|
+
});
|
|
120
|
+
return r.data().count;
|
|
121
|
+
}
|
|
93
122
|
const r = await firestoreQuery.count().get();
|
|
94
123
|
return r.data().count;
|
|
95
124
|
}
|
|
@@ -101,7 +130,7 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
101
130
|
...this.cfg.streamOptions,
|
|
102
131
|
...opt_,
|
|
103
132
|
};
|
|
104
|
-
if (opt.experimentalCursorStream) {
|
|
133
|
+
if (opt.experimentalCursorStream || opt.readAt) {
|
|
105
134
|
return Pipeline.from(new FirestoreStreamReadable(firestoreQuery, q, opt));
|
|
106
135
|
}
|
|
107
136
|
if (opt.experimentalShardedStream) {
|
|
@@ -128,7 +157,7 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
128
157
|
}
|
|
129
158
|
return;
|
|
130
159
|
}
|
|
131
|
-
await pMap(_chunk(rows, MAX_ITEMS), async chunk => {
|
|
160
|
+
await pMap(_chunk(rows, MAX_ITEMS), async (chunk) => {
|
|
132
161
|
// .batch is called "Atomic batch writer"
|
|
133
162
|
// Executes multiple writes in a single atomic transaction-like commit — all succeed or all fail.
|
|
134
163
|
// If any write in the batch fails (e.g., permission error, missing doc), the whole batch fails.
|
|
@@ -163,7 +192,7 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
163
192
|
tableRows.push([table, row]);
|
|
164
193
|
}
|
|
165
194
|
}
|
|
166
|
-
await pMap(_chunk(tableRows, MAX_ITEMS), async chunk => {
|
|
195
|
+
await pMap(_chunk(tableRows, MAX_ITEMS), async (chunk) => {
|
|
167
196
|
const batch = firestore.batch();
|
|
168
197
|
for (const [table, row] of chunk) {
|
|
169
198
|
_assert(row.id, `firestore-db doesn't support id auto-generation, but empty id was provided in multiSaveBatch`);
|
|
@@ -207,7 +236,7 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
207
236
|
}
|
|
208
237
|
return ids.length;
|
|
209
238
|
}
|
|
210
|
-
await pMap(_chunk(ids, MAX_ITEMS), async chunk => {
|
|
239
|
+
await pMap(_chunk(ids, MAX_ITEMS), async (chunk) => {
|
|
211
240
|
const batch = firestore.batch();
|
|
212
241
|
for (const id of chunk) {
|
|
213
242
|
batch.delete(col.doc(escapeDocId(id)));
|
|
@@ -230,7 +259,7 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
230
259
|
}
|
|
231
260
|
}
|
|
232
261
|
else {
|
|
233
|
-
await pMap(_chunk(refs, MAX_ITEMS), async chunk => {
|
|
262
|
+
await pMap(_chunk(refs, MAX_ITEMS), async (chunk) => {
|
|
234
263
|
const batch = firestore.batch();
|
|
235
264
|
for (const ref of chunk) {
|
|
236
265
|
batch.delete(ref);
|
|
@@ -249,7 +278,7 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
249
278
|
async runInTransaction(fn, opt = {}) {
|
|
250
279
|
const { readOnly } = opt;
|
|
251
280
|
try {
|
|
252
|
-
await this.cfg.firestore.runTransaction(async firestoreTx => {
|
|
281
|
+
await this.cfg.firestore.runTransaction(async (firestoreTx) => {
|
|
253
282
|
const tx = new FirestoreDBTransaction(this, firestoreTx);
|
|
254
283
|
await fn(tx);
|
|
255
284
|
}, {
|
|
@@ -18,6 +18,7 @@ export declare class FirestoreStreamReadable<T extends ObjectWithId = any> exten
|
|
|
18
18
|
* For debugging.
|
|
19
19
|
*/
|
|
20
20
|
countReads: number;
|
|
21
|
+
private readonly readTime?;
|
|
21
22
|
private readonly opt;
|
|
22
23
|
private logger;
|
|
23
24
|
constructor(q: Query, dbQuery: DBQuery<T>, opt: FirestoreDBStreamOptions);
|
|
@@ -6,6 +6,7 @@ import { TimeoutError } from '@naturalcycles/js-lib/error/error.util.js';
|
|
|
6
6
|
import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log';
|
|
7
7
|
import { pRetry } from '@naturalcycles/js-lib/promise/pRetry.js';
|
|
8
8
|
import { unescapeDocId } from './firestore.util.js';
|
|
9
|
+
import { readAtToReadTime } from './query.util.js';
|
|
9
10
|
export class FirestoreStreamReadable extends Readable {
|
|
10
11
|
q;
|
|
11
12
|
table;
|
|
@@ -20,6 +21,7 @@ export class FirestoreStreamReadable extends Readable {
|
|
|
20
21
|
* For debugging.
|
|
21
22
|
*/
|
|
22
23
|
countReads = 0;
|
|
24
|
+
readTime;
|
|
23
25
|
opt;
|
|
24
26
|
logger;
|
|
25
27
|
constructor(q, dbQuery, opt) {
|
|
@@ -34,7 +36,7 @@ export class FirestoreStreamReadable extends Readable {
|
|
|
34
36
|
batchSize,
|
|
35
37
|
highWaterMark,
|
|
36
38
|
};
|
|
37
|
-
|
|
39
|
+
this.readTime = readAtToReadTime(opt);
|
|
38
40
|
this.originalLimit = dbQuery._limitValue;
|
|
39
41
|
this.table = dbQuery.table;
|
|
40
42
|
const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel);
|
|
@@ -135,6 +137,12 @@ export class FirestoreStreamReadable extends Readable {
|
|
|
135
137
|
const { table, logger } = this;
|
|
136
138
|
try {
|
|
137
139
|
return await pRetry(async () => {
|
|
140
|
+
if (this.readTime) {
|
|
141
|
+
return await this.q.firestore.runTransaction(tx => tx.get(q), {
|
|
142
|
+
readOnly: true,
|
|
143
|
+
readTime: this.readTime,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
138
146
|
return await q.get();
|
|
139
147
|
}, {
|
|
140
148
|
name: `FirestoreStreamReadable.query(${table})`,
|
package/dist/query.util.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { Timestamp } from '@google-cloud/firestore';
|
|
1
2
|
import type { Query } from '@google-cloud/firestore';
|
|
2
|
-
import type { DBQuery } from '@naturalcycles/db-lib';
|
|
3
|
+
import type { CommonDBReadOptions, DBQuery } from '@naturalcycles/db-lib';
|
|
3
4
|
import type { ObjectWithId } from '@naturalcycles/js-lib/types';
|
|
5
|
+
export declare function readAtToReadTime(opt: CommonDBReadOptions): Timestamp | undefined;
|
|
4
6
|
export declare function dbQueryToFirestoreQuery<ROW extends ObjectWithId>(dbQuery: DBQuery<ROW>, emptyQuery: Query): Query;
|
package/dist/query.util.js
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import { FieldPath } from '@google-cloud/firestore';
|
|
1
|
+
import { FieldPath, Timestamp } from '@google-cloud/firestore';
|
|
2
|
+
import { _round } from '@naturalcycles/js-lib';
|
|
3
|
+
export function readAtToReadTime(opt) {
|
|
4
|
+
if (!opt.readAt)
|
|
5
|
+
return;
|
|
6
|
+
// Same logic as Datastore: round to whole minutes, guard against future
|
|
7
|
+
let readTimeMs = _round(opt.readAt, 60) * 1000;
|
|
8
|
+
if (readTimeMs >= Date.now() - 1000) {
|
|
9
|
+
readTimeMs -= 60_000;
|
|
10
|
+
}
|
|
11
|
+
return Timestamp.fromMillis(readTimeMs);
|
|
12
|
+
}
|
|
2
13
|
// Map DBQueryFilterOp to WhereFilterOp
|
|
3
14
|
// Currently it's fully aligned!
|
|
4
|
-
const OP_MAP = {
|
|
15
|
+
const OP_MAP = {
|
|
16
|
+
// '=': '==',
|
|
17
|
+
// in: 'array-contains',
|
|
18
|
+
};
|
|
5
19
|
export function dbQueryToFirestoreQuery(dbQuery, emptyQuery) {
|
|
6
20
|
let q = emptyQuery;
|
|
7
21
|
// filter
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@naturalcycles/firestore-lib",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"dependencies": {
|
|
5
|
-
"@google-cloud/firestore": "^
|
|
5
|
+
"@google-cloud/firestore": "^8",
|
|
6
6
|
"@naturalcycles/db-lib": "^10",
|
|
7
7
|
"@naturalcycles/js-lib": "^15",
|
|
8
8
|
"@naturalcycles/nodejs-lib": "^15",
|
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@types/node": "^25",
|
|
13
|
-
"@typescript/native-preview": "
|
|
14
|
-
"
|
|
15
|
-
"firebase-admin": "^13",
|
|
13
|
+
"@typescript/native-preview": "beta",
|
|
14
|
+
"firebase-admin": "^14",
|
|
16
15
|
"@naturalcycles/dev-lib": "18.4.2"
|
|
17
16
|
},
|
|
18
17
|
"exports": {
|
|
@@ -39,7 +38,7 @@
|
|
|
39
38
|
"engines": {
|
|
40
39
|
"node": ">=24.10.0"
|
|
41
40
|
},
|
|
42
|
-
"version": "2.
|
|
41
|
+
"version": "2.16.0",
|
|
43
42
|
"description": "Firestore implementation of CommonDB interface",
|
|
44
43
|
"author": "Natural Cycles Team",
|
|
45
44
|
"license": "MIT",
|
package/src/firestore.db.ts
CHANGED
|
@@ -35,7 +35,7 @@ import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream'
|
|
|
35
35
|
import { escapeDocId, unescapeDocId } from './firestore.util.js'
|
|
36
36
|
import { FirestoreShardedReadable } from './firestoreShardedReadable.js'
|
|
37
37
|
import { FirestoreStreamReadable } from './firestoreStreamReadable.js'
|
|
38
|
-
import { dbQueryToFirestoreQuery } from './query.util.js'
|
|
38
|
+
import { dbQueryToFirestoreQuery, readAtToReadTime } from './query.util.js'
|
|
39
39
|
|
|
40
40
|
export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
41
41
|
constructor(cfg: FirestoreDBCfg) {
|
|
@@ -63,16 +63,20 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
63
63
|
): Promise<ROW[]> {
|
|
64
64
|
if (!ids.length) return []
|
|
65
65
|
|
|
66
|
-
// todo: support PITR: https://firebase.google.com/docs/firestore/enterprise/use-pitr#read-pitr
|
|
67
|
-
|
|
68
66
|
const { firestore } = this.cfg
|
|
69
67
|
const col = firestore.collection(table)
|
|
68
|
+
const refs = ids.map(id => col.doc(escapeDocId(id)))
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
const readTime = readAtToReadTime(opt)
|
|
71
|
+
if (readTime) {
|
|
72
|
+
_assert(!opt.tx, 'readAt is not supported inside an existing transaction')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const snapshots = await (readTime
|
|
76
|
+
? firestore.runTransaction(tx => tx.getAll(...refs), { readOnly: true, readTime })
|
|
77
|
+
: ((opt.tx as FirestoreDBTransaction)?.tx || firestore).getAll(...refs))
|
|
78
|
+
|
|
79
|
+
return snapshots
|
|
76
80
|
.map(doc => {
|
|
77
81
|
const data = doc.data()
|
|
78
82
|
if (data === undefined) return
|
|
@@ -97,7 +101,15 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
97
101
|
refs.push(...ids.map(id => col.doc(escapeDocId(id))))
|
|
98
102
|
}
|
|
99
103
|
|
|
100
|
-
const
|
|
104
|
+
const readTime = readAtToReadTime(opt)
|
|
105
|
+
if (readTime) {
|
|
106
|
+
_assert(!opt.tx, 'readAt is not supported inside an existing transaction')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const snapshots = await (readTime
|
|
110
|
+
? firestore.runTransaction(tx => tx.getAll(...refs), { readOnly: true, readTime })
|
|
111
|
+
: ((opt.tx as FirestoreDBTransaction)?.tx || firestore).getAll(...refs))
|
|
112
|
+
|
|
101
113
|
snapshots.forEach(snap => {
|
|
102
114
|
const data = snap.data()
|
|
103
115
|
if (data === undefined) return
|
|
@@ -115,7 +127,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
115
127
|
// QUERY
|
|
116
128
|
override async runQuery<ROW extends ObjectWithId>(
|
|
117
129
|
q: DBQuery<ROW>,
|
|
118
|
-
opt
|
|
130
|
+
opt: FirestoreDBReadOptions = {},
|
|
119
131
|
): Promise<RunQueryResult<ROW>> {
|
|
120
132
|
const idFilter = q._filters.find(f => f.name === 'id')
|
|
121
133
|
if (idFilter) {
|
|
@@ -127,7 +139,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
127
139
|
|
|
128
140
|
const firestoreQuery = dbQueryToFirestoreQuery(q, this.cfg.firestore.collection(q.table))
|
|
129
141
|
|
|
130
|
-
let rows = await this.runFirestoreQuery<ROW>(firestoreQuery)
|
|
142
|
+
let rows = await this.runFirestoreQuery<ROW>(firestoreQuery, opt)
|
|
131
143
|
|
|
132
144
|
// Special case when projection query didn't specify 'id'
|
|
133
145
|
if (q._selectedFieldNames && !q._selectedFieldNames.includes('id')) {
|
|
@@ -137,15 +149,36 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
137
149
|
return { rows }
|
|
138
150
|
}
|
|
139
151
|
|
|
140
|
-
async runFirestoreQuery<ROW extends ObjectWithId>(
|
|
152
|
+
async runFirestoreQuery<ROW extends ObjectWithId>(
|
|
153
|
+
q: Query,
|
|
154
|
+
opt: FirestoreDBReadOptions = {},
|
|
155
|
+
): Promise<ROW[]> {
|
|
156
|
+
const readTime = readAtToReadTime(opt)
|
|
157
|
+
if (readTime) {
|
|
158
|
+
const qs = await this.cfg.firestore.runTransaction(tx => tx.get(q), {
|
|
159
|
+
readOnly: true,
|
|
160
|
+
readTime,
|
|
161
|
+
})
|
|
162
|
+
return this.querySnapshotToArray(qs)
|
|
163
|
+
}
|
|
141
164
|
return this.querySnapshotToArray(await q.get())
|
|
142
165
|
}
|
|
143
166
|
|
|
144
167
|
override async runQueryCount<ROW extends ObjectWithId>(
|
|
145
168
|
q: DBQuery<ROW>,
|
|
146
|
-
|
|
169
|
+
opt: FirestoreDBReadOptions = {},
|
|
147
170
|
): Promise<number> {
|
|
148
171
|
const firestoreQuery = dbQueryToFirestoreQuery(q, this.cfg.firestore.collection(q.table))
|
|
172
|
+
|
|
173
|
+
const readTime = readAtToReadTime(opt)
|
|
174
|
+
if (readTime) {
|
|
175
|
+
const r = await this.cfg.firestore.runTransaction(tx => tx.get(firestoreQuery.count()), {
|
|
176
|
+
readOnly: true,
|
|
177
|
+
readTime,
|
|
178
|
+
})
|
|
179
|
+
return r.data().count
|
|
180
|
+
}
|
|
181
|
+
|
|
149
182
|
const r = await firestoreQuery.count().get()
|
|
150
183
|
return r.data().count
|
|
151
184
|
}
|
|
@@ -163,7 +196,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
163
196
|
...opt_,
|
|
164
197
|
}
|
|
165
198
|
|
|
166
|
-
if (opt.experimentalCursorStream) {
|
|
199
|
+
if (opt.experimentalCursorStream || opt.readAt) {
|
|
167
200
|
return Pipeline.from(new FirestoreStreamReadable(firestoreQuery, q, opt))
|
|
168
201
|
}
|
|
169
202
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Readable } from 'node:stream'
|
|
2
2
|
import { FieldPath } from '@google-cloud/firestore'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
Query,
|
|
5
|
+
QueryDocumentSnapshot,
|
|
6
|
+
QuerySnapshot,
|
|
7
|
+
Timestamp,
|
|
8
|
+
} from '@google-cloud/firestore'
|
|
4
9
|
import type { DBQuery } from '@naturalcycles/db-lib'
|
|
5
10
|
import { localTime } from '@naturalcycles/js-lib/datetime/localTime.js'
|
|
6
11
|
import { _ms } from '@naturalcycles/js-lib/datetime/time.util.js'
|
|
@@ -12,6 +17,7 @@ import type { ObjectWithId } from '@naturalcycles/js-lib/types'
|
|
|
12
17
|
import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream'
|
|
13
18
|
import type { FirestoreDBStreamOptions } from './firestore.db.js'
|
|
14
19
|
import { unescapeDocId } from './firestore.util.js'
|
|
20
|
+
import { readAtToReadTime } from './query.util.js'
|
|
15
21
|
|
|
16
22
|
export class FirestoreStreamReadable<T extends ObjectWithId = any>
|
|
17
23
|
extends Readable
|
|
@@ -30,6 +36,7 @@ export class FirestoreStreamReadable<T extends ObjectWithId = any>
|
|
|
30
36
|
*/
|
|
31
37
|
countReads = 0
|
|
32
38
|
|
|
39
|
+
private readonly readTime?: Timestamp
|
|
33
40
|
private readonly opt: FirestoreDBStreamOptions & { batchSize: number; highWaterMark: number }
|
|
34
41
|
private logger: CommonLogger
|
|
35
42
|
|
|
@@ -49,7 +56,7 @@ export class FirestoreStreamReadable<T extends ObjectWithId = any>
|
|
|
49
56
|
batchSize,
|
|
50
57
|
highWaterMark,
|
|
51
58
|
}
|
|
52
|
-
|
|
59
|
+
this.readTime = readAtToReadTime(opt)
|
|
53
60
|
|
|
54
61
|
this.originalLimit = dbQuery._limitValue
|
|
55
62
|
this.table = dbQuery.table
|
|
@@ -176,6 +183,12 @@ export class FirestoreStreamReadable<T extends ObjectWithId = any>
|
|
|
176
183
|
try {
|
|
177
184
|
return await pRetry(
|
|
178
185
|
async () => {
|
|
186
|
+
if (this.readTime) {
|
|
187
|
+
return await this.q.firestore.runTransaction(tx => tx.get(q), {
|
|
188
|
+
readOnly: true,
|
|
189
|
+
readTime: this.readTime,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
179
192
|
return await q.get()
|
|
180
193
|
},
|
|
181
194
|
{
|
package/src/index.ts
CHANGED
package/src/query.util.ts
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
import { FieldPath } from '@google-cloud/firestore'
|
|
1
|
+
import { FieldPath, Timestamp } from '@google-cloud/firestore'
|
|
2
2
|
import type { Query, WhereFilterOp } from '@google-cloud/firestore'
|
|
3
|
-
import type { DBQuery, DBQueryFilterOperator } from '@naturalcycles/db-lib'
|
|
3
|
+
import type { CommonDBReadOptions, DBQuery, DBQueryFilterOperator } from '@naturalcycles/db-lib'
|
|
4
|
+
import { _round } from '@naturalcycles/js-lib'
|
|
4
5
|
import type { ObjectWithId } from '@naturalcycles/js-lib/types'
|
|
5
6
|
|
|
7
|
+
export function readAtToReadTime(opt: CommonDBReadOptions): Timestamp | undefined {
|
|
8
|
+
if (!opt.readAt) return
|
|
9
|
+
|
|
10
|
+
// Same logic as Datastore: round to whole minutes, guard against future
|
|
11
|
+
let readTimeMs = _round(opt.readAt, 60) * 1000
|
|
12
|
+
if (readTimeMs >= Date.now() - 1000) {
|
|
13
|
+
readTimeMs -= 60_000
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return Timestamp.fromMillis(readTimeMs)
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
// Map DBQueryFilterOp to WhereFilterOp
|
|
7
20
|
// Currently it's fully aligned!
|
|
8
21
|
const OP_MAP: Partial<Record<DBQueryFilterOperator, WhereFilterOp>> = {
|