@naturalcycles/firestore-lib 2.6.0 → 2.8.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 +85 -21
- package/dist/firestore.db.js +29 -16
- package/dist/firestoreStreamReadable.d.ts +28 -0
- package/dist/firestoreStreamReadable.js +137 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/query.util.js +9 -0
- package/package.json +2 -2
- package/src/firestore.db.ts +123 -31
- package/src/firestoreStreamReadable.ts +183 -0
- package/src/index.ts +1 -0
- package/src/query.util.ts +11 -0
package/dist/firestore.db.d.ts
CHANGED
|
@@ -1,37 +1,28 @@
|
|
|
1
|
-
import type { Firestore, Query, Transaction } from '@google-cloud/firestore';
|
|
2
|
-
import type { CommonDB, CommonDBOptions, CommonDBReadOptions, CommonDBSaveOptions,
|
|
1
|
+
import type { Firestore, Query, QuerySnapshot, Transaction } from '@google-cloud/firestore';
|
|
2
|
+
import type { CommonDB, CommonDBOptions, CommonDBReadOptions, CommonDBSaveOptions, CommonDBSupport, CommonDBTransactionOptions, DBQuery, DBTransaction, DBTransactionFn, RunQueryResult } from '@naturalcycles/db-lib';
|
|
3
3
|
import { BaseCommonDB } from '@naturalcycles/db-lib';
|
|
4
|
-
import
|
|
4
|
+
import { type CommonLogger } from '@naturalcycles/js-lib/log';
|
|
5
|
+
import type { NumberOfSeconds, ObjectWithId, StringMap } from '@naturalcycles/js-lib/types';
|
|
5
6
|
import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream';
|
|
6
|
-
export interface FirestoreDBCfg {
|
|
7
|
-
firestore: Firestore;
|
|
8
|
-
}
|
|
9
|
-
export interface FirestoreDBOptions extends CommonDBOptions {
|
|
10
|
-
}
|
|
11
|
-
export interface FirestoreDBReadOptions extends CommonDBReadOptions {
|
|
12
|
-
}
|
|
13
|
-
export interface FirestoreDBSaveOptions<ROW extends ObjectWithId> extends CommonDBSaveOptions<ROW> {
|
|
14
|
-
}
|
|
15
|
-
export declare class RollbackError extends Error {
|
|
16
|
-
constructor();
|
|
17
|
-
}
|
|
18
7
|
export declare class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
19
|
-
cfg: FirestoreDBCfg;
|
|
20
8
|
constructor(cfg: FirestoreDBCfg);
|
|
9
|
+
cfg: FirestoreDBCfg & {
|
|
10
|
+
logger: CommonLogger;
|
|
11
|
+
};
|
|
21
12
|
support: CommonDBSupport;
|
|
22
13
|
getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: FirestoreDBReadOptions): Promise<ROW[]>;
|
|
23
|
-
|
|
14
|
+
multiGet<ROW extends ObjectWithId>(map: StringMap<string[]>, opt?: CommonDBReadOptions): Promise<StringMap<ROW[]>>;
|
|
24
15
|
runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: FirestoreDBOptions): Promise<RunQueryResult<ROW>>;
|
|
25
16
|
runFirestoreQuery<ROW extends ObjectWithId>(q: Query): Promise<ROW[]>;
|
|
26
17
|
runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: FirestoreDBOptions): Promise<number>;
|
|
27
|
-
streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>,
|
|
18
|
+
streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt_?: FirestoreDBStreamOptions): ReadableTyped<ROW>;
|
|
28
19
|
saveBatch<ROW extends ObjectWithId>(table: string, rows: ROW[], opt?: FirestoreDBSaveOptions<ROW>): Promise<void>;
|
|
29
|
-
|
|
20
|
+
multiSave<ROW extends ObjectWithId>(map: StringMap<ROW[]>, opt?: FirestoreDBSaveOptions<ROW>): Promise<void>;
|
|
30
21
|
patchById<ROW extends ObjectWithId>(table: string, id: string, patch: Partial<ROW>, opt?: FirestoreDBOptions): Promise<void>;
|
|
31
22
|
deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: FirestoreDBOptions): Promise<number>;
|
|
32
23
|
deleteByIds(table: string, ids: string[], opt?: FirestoreDBOptions): Promise<number>;
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
multiDelete(map: StringMap<string[]>, opt?: FirestoreDBOptions): Promise<number>;
|
|
25
|
+
querySnapshotToArray<T = any>(qs: QuerySnapshot): T[];
|
|
35
26
|
runInTransaction(fn: DBTransactionFn, opt?: CommonDBTransactionOptions): Promise<void>;
|
|
36
27
|
/**
|
|
37
28
|
* Caveat: it always returns an empty object, not the actual incrementMap.
|
|
@@ -53,3 +44,76 @@ export declare class FirestoreDBTransaction implements DBTransaction {
|
|
|
53
44
|
saveBatch<ROW extends ObjectWithId>(table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
|
|
54
45
|
deleteByIds(table: string, ids: string[], opt?: CommonDBOptions): Promise<number>;
|
|
55
46
|
}
|
|
47
|
+
export interface FirestoreDBCfg {
|
|
48
|
+
firestore: Firestore;
|
|
49
|
+
/**
|
|
50
|
+
* Use it to set default options to stream operations,
|
|
51
|
+
* e.g you can globally enable `experimentalCursorStream` here, set the batchSize, etc.
|
|
52
|
+
*/
|
|
53
|
+
streamOptions?: FirestoreDBStreamOptions;
|
|
54
|
+
/**
|
|
55
|
+
* Default to `console`
|
|
56
|
+
*/
|
|
57
|
+
logger?: CommonLogger;
|
|
58
|
+
}
|
|
59
|
+
export declare class RollbackError extends Error {
|
|
60
|
+
constructor();
|
|
61
|
+
}
|
|
62
|
+
export interface FirestoreDBStreamOptions extends FirestoreDBReadOptions {
|
|
63
|
+
/**
|
|
64
|
+
* Set to `true` to stream via experimental "cursor-query based stream".
|
|
65
|
+
*
|
|
66
|
+
* Defaults to false
|
|
67
|
+
*/
|
|
68
|
+
experimentalCursorStream?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Applicable to `experimentalCursorStream`.
|
|
71
|
+
* Defines the size (limit) of each individual query.
|
|
72
|
+
*
|
|
73
|
+
* Default: 1000
|
|
74
|
+
*/
|
|
75
|
+
batchSize?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Applicable to `experimentalCursorStream`
|
|
78
|
+
*
|
|
79
|
+
* Set to a value (number of Megabytes) to control the peak RSS size.
|
|
80
|
+
* If limit is reached - streaming will pause until the stream keeps up, and then
|
|
81
|
+
* resumes.
|
|
82
|
+
*
|
|
83
|
+
* Set to 0/undefined to disable. Stream will get "slow" then, cause it'll only run the query
|
|
84
|
+
* when _read is called.
|
|
85
|
+
*
|
|
86
|
+
* @default 1000
|
|
87
|
+
*/
|
|
88
|
+
rssLimitMB?: number;
|
|
89
|
+
/**
|
|
90
|
+
* Applicable to `experimentalCursorStream`
|
|
91
|
+
* Default false.
|
|
92
|
+
* If true, stream will pause until consumer requests more data (via _read).
|
|
93
|
+
* It means it'll run slower, as buffer will be equal to batchSize (1000) at max.
|
|
94
|
+
* There will be gaps in time between "last query loaded" and "next query requested".
|
|
95
|
+
* This mode is useful e.g for DB migrations, where you want to avoid "stale data".
|
|
96
|
+
* So, it minimizes the time between "item loaded" and "item saved" during DB migration.
|
|
97
|
+
*/
|
|
98
|
+
singleBatchBuffer?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Set to `true` to log additional debug info, when using experimentalCursorStream.
|
|
101
|
+
*
|
|
102
|
+
* @default false
|
|
103
|
+
*/
|
|
104
|
+
debug?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Default is undefined.
|
|
107
|
+
* If set - sets a "safety timer", which will force call _read after the specified number of seconds.
|
|
108
|
+
* This is to prevent possible "dead-lock"/race-condition that would make the stream "hang".
|
|
109
|
+
*
|
|
110
|
+
* @experimental
|
|
111
|
+
*/
|
|
112
|
+
maxWait?: NumberOfSeconds;
|
|
113
|
+
}
|
|
114
|
+
export interface FirestoreDBOptions extends CommonDBOptions {
|
|
115
|
+
}
|
|
116
|
+
export interface FirestoreDBReadOptions extends CommonDBReadOptions {
|
|
117
|
+
}
|
|
118
|
+
export interface FirestoreDBSaveOptions<ROW extends ObjectWithId> extends CommonDBSaveOptions<ROW> {
|
|
119
|
+
}
|
package/dist/firestore.db.js
CHANGED
|
@@ -3,27 +3,22 @@ import { BaseCommonDB, commonDBFullSupport } from '@naturalcycles/db-lib';
|
|
|
3
3
|
import { _isTruthy } from '@naturalcycles/js-lib';
|
|
4
4
|
import { _chunk } from '@naturalcycles/js-lib/array/array.util.js';
|
|
5
5
|
import { _assert } from '@naturalcycles/js-lib/error/assert.js';
|
|
6
|
+
import { commonLoggerMinLevel } from '@naturalcycles/js-lib/log';
|
|
6
7
|
import { _filterUndefinedValues, _omit } from '@naturalcycles/js-lib/object/object.util.js';
|
|
7
8
|
import { pMap } from '@naturalcycles/js-lib/promise/pMap.js';
|
|
8
9
|
import { _stringMapEntries } from '@naturalcycles/js-lib/types';
|
|
9
10
|
import { escapeDocId, unescapeDocId } from './firestore.util.js';
|
|
11
|
+
import { FirestoreStreamReadable } from './firestoreStreamReadable.js';
|
|
10
12
|
import { dbQueryToFirestoreQuery } from './query.util.js';
|
|
11
|
-
const methodMap = {
|
|
12
|
-
insert: 'create',
|
|
13
|
-
update: 'update',
|
|
14
|
-
upsert: 'set',
|
|
15
|
-
};
|
|
16
|
-
export class RollbackError extends Error {
|
|
17
|
-
constructor() {
|
|
18
|
-
super('rollback');
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
13
|
export class FirestoreDB extends BaseCommonDB {
|
|
22
|
-
cfg;
|
|
23
14
|
constructor(cfg) {
|
|
24
15
|
super();
|
|
25
|
-
this.cfg =
|
|
16
|
+
this.cfg = {
|
|
17
|
+
logger: console,
|
|
18
|
+
...cfg,
|
|
19
|
+
};
|
|
26
20
|
}
|
|
21
|
+
cfg;
|
|
27
22
|
support = {
|
|
28
23
|
...commonDBFullSupport,
|
|
29
24
|
patchByQuery: false, // todo: can be implemented
|
|
@@ -33,6 +28,7 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
33
28
|
async getByIds(table, ids, opt = {}) {
|
|
34
29
|
if (!ids.length)
|
|
35
30
|
return [];
|
|
31
|
+
// todo: support PITR: https://firebase.google.com/docs/firestore/enterprise/use-pitr#read-pitr
|
|
36
32
|
const { firestore } = this.cfg;
|
|
37
33
|
const col = firestore.collection(table);
|
|
38
34
|
return (await (opt.tx?.tx || firestore).getAll(...ids.map(id => col.doc(escapeDocId(id)))))
|
|
@@ -47,7 +43,7 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
47
43
|
})
|
|
48
44
|
.filter(_isTruthy);
|
|
49
45
|
}
|
|
50
|
-
async
|
|
46
|
+
async multiGet(map, opt = {}) {
|
|
51
47
|
const result = {};
|
|
52
48
|
const { firestore } = this.cfg;
|
|
53
49
|
const refs = [];
|
|
@@ -95,8 +91,15 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
95
91
|
const r = await firestoreQuery.count().get();
|
|
96
92
|
return r.data().count;
|
|
97
93
|
}
|
|
98
|
-
streamQuery(q,
|
|
94
|
+
streamQuery(q, opt_) {
|
|
99
95
|
const firestoreQuery = dbQueryToFirestoreQuery(q, this.cfg.firestore.collection(q.table));
|
|
96
|
+
const opt = {
|
|
97
|
+
...this.cfg.streamOptions,
|
|
98
|
+
...opt_,
|
|
99
|
+
};
|
|
100
|
+
if (opt.experimentalCursorStream) {
|
|
101
|
+
return new FirestoreStreamReadable(firestoreQuery, q, opt, commonLoggerMinLevel(this.cfg.logger, opt.debug ? 'log' : 'warn'));
|
|
102
|
+
}
|
|
100
103
|
return firestoreQuery.stream().map(doc => {
|
|
101
104
|
return {
|
|
102
105
|
id: unescapeDocId(doc.id),
|
|
@@ -132,7 +135,7 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
132
135
|
await batch.commit();
|
|
133
136
|
}, { concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY });
|
|
134
137
|
}
|
|
135
|
-
async
|
|
138
|
+
async multiSave(map, opt = {}) {
|
|
136
139
|
const { firestore } = this.cfg;
|
|
137
140
|
const method = methodMap[opt.saveMethod] || 'set';
|
|
138
141
|
if (opt.tx) {
|
|
@@ -206,7 +209,7 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
206
209
|
}, { concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY });
|
|
207
210
|
return ids.length;
|
|
208
211
|
}
|
|
209
|
-
async
|
|
212
|
+
async multiDelete(map, opt = {}) {
|
|
210
213
|
const { firestore } = this.cfg;
|
|
211
214
|
const refs = [];
|
|
212
215
|
for (const [table, ids] of _stringMapEntries(map)) {
|
|
@@ -306,3 +309,13 @@ export class FirestoreDBTransaction {
|
|
|
306
309
|
const MAX_ITEMS = 500;
|
|
307
310
|
// It's an empyrical value, but anything less than infinity is better than infinity
|
|
308
311
|
const FIRESTORE_RECOMMENDED_CONCURRENCY = 8;
|
|
312
|
+
const methodMap = {
|
|
313
|
+
insert: 'create',
|
|
314
|
+
update: 'update',
|
|
315
|
+
upsert: 'set',
|
|
316
|
+
};
|
|
317
|
+
export class RollbackError extends Error {
|
|
318
|
+
constructor() {
|
|
319
|
+
super('rollback');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { type Query } from '@google-cloud/firestore';
|
|
3
|
+
import type { DBQuery } from '@naturalcycles/db-lib';
|
|
4
|
+
import type { CommonLogger } from '@naturalcycles/js-lib/log';
|
|
5
|
+
import type { ObjectWithId } from '@naturalcycles/js-lib/types';
|
|
6
|
+
import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream';
|
|
7
|
+
import type { FirestoreDBStreamOptions } from './firestore.db.js';
|
|
8
|
+
export declare class FirestoreStreamReadable<T extends ObjectWithId = any> extends Readable implements ReadableTyped<T> {
|
|
9
|
+
private q;
|
|
10
|
+
private logger;
|
|
11
|
+
private readonly table;
|
|
12
|
+
private readonly originalLimit;
|
|
13
|
+
private rowsRetrieved;
|
|
14
|
+
private endCursor?;
|
|
15
|
+
private running;
|
|
16
|
+
private done;
|
|
17
|
+
private lastQueryDone?;
|
|
18
|
+
private totalWait;
|
|
19
|
+
private readonly opt;
|
|
20
|
+
constructor(q: Query, dbQuery: DBQuery<T>, opt: FirestoreDBStreamOptions, logger: CommonLogger);
|
|
21
|
+
/**
|
|
22
|
+
* Counts how many times _read was called.
|
|
23
|
+
* For debugging.
|
|
24
|
+
*/
|
|
25
|
+
count: number;
|
|
26
|
+
_read(): void;
|
|
27
|
+
private runNextQuery;
|
|
28
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { FieldPath } from '@google-cloud/firestore';
|
|
3
|
+
import { _ms } from '@naturalcycles/js-lib/datetime/time.util.js';
|
|
4
|
+
import { pRetry } from '@naturalcycles/js-lib/promise/pRetry.js';
|
|
5
|
+
import { unescapeDocId } from './firestore.util.js';
|
|
6
|
+
export class FirestoreStreamReadable extends Readable {
|
|
7
|
+
q;
|
|
8
|
+
logger;
|
|
9
|
+
table;
|
|
10
|
+
originalLimit;
|
|
11
|
+
rowsRetrieved = 0;
|
|
12
|
+
endCursor;
|
|
13
|
+
running = false;
|
|
14
|
+
done = false;
|
|
15
|
+
lastQueryDone;
|
|
16
|
+
totalWait = 0;
|
|
17
|
+
opt;
|
|
18
|
+
// private readonly dsOpt: RunQueryOptions
|
|
19
|
+
constructor(q, dbQuery, opt, logger) {
|
|
20
|
+
super({ objectMode: true });
|
|
21
|
+
this.q = q;
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
this.opt = {
|
|
24
|
+
rssLimitMB: 1000,
|
|
25
|
+
batchSize: 1000,
|
|
26
|
+
...opt,
|
|
27
|
+
};
|
|
28
|
+
// todo: support PITR!
|
|
29
|
+
// this.dsOpt = {}
|
|
30
|
+
// if (opt.readAt) {
|
|
31
|
+
// // Datastore expects UnixTimestamp in milliseconds
|
|
32
|
+
// this.dsOpt.readTime = opt.readAt * 1000
|
|
33
|
+
// }
|
|
34
|
+
this.originalLimit = dbQuery._limitValue;
|
|
35
|
+
this.table = dbQuery.table;
|
|
36
|
+
logger.log(`!! using experimentalCursorStream !! ${this.table}, batchSize: ${this.opt.batchSize}`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Counts how many times _read was called.
|
|
40
|
+
* For debugging.
|
|
41
|
+
*/
|
|
42
|
+
count = 0;
|
|
43
|
+
_read() {
|
|
44
|
+
// this.lastReadTimestamp = Date.now() as UnixTimestampMillis
|
|
45
|
+
// console.log(`_read called ${++this.count}, wasRunning: ${this.running}`) // debugging
|
|
46
|
+
this.count++;
|
|
47
|
+
if (this.done) {
|
|
48
|
+
this.logger.warn(`!!! _read was called, but done==true`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!this.running) {
|
|
52
|
+
void this.runNextQuery().catch(err => {
|
|
53
|
+
console.log('error in runNextQuery', err);
|
|
54
|
+
this.emit('error', err);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
this.logger.log(`_read ${this.count}, wasRunning: true`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async runNextQuery() {
|
|
62
|
+
if (this.done)
|
|
63
|
+
return;
|
|
64
|
+
if (this.lastQueryDone) {
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
this.totalWait += now - this.lastQueryDone;
|
|
67
|
+
}
|
|
68
|
+
this.running = true;
|
|
69
|
+
let limit = this.opt.batchSize;
|
|
70
|
+
if (this.originalLimit) {
|
|
71
|
+
limit = Math.min(this.opt.batchSize, this.originalLimit - this.rowsRetrieved);
|
|
72
|
+
}
|
|
73
|
+
// console.log(`limit: ${limit}`)
|
|
74
|
+
// We have to orderBy documentId, to be able to use id as a cursor
|
|
75
|
+
let q = this.q.orderBy(FieldPath.documentId()).limit(limit);
|
|
76
|
+
if (this.endCursor) {
|
|
77
|
+
q = q.startAfter(this.endCursor);
|
|
78
|
+
}
|
|
79
|
+
let qs;
|
|
80
|
+
try {
|
|
81
|
+
await pRetry(async () => {
|
|
82
|
+
qs = await q.get();
|
|
83
|
+
}, {
|
|
84
|
+
name: `FirestoreStreamReadable.query(${this.table})`,
|
|
85
|
+
maxAttempts: 5,
|
|
86
|
+
delay: 5000,
|
|
87
|
+
delayMultiplier: 2,
|
|
88
|
+
logger: this.logger,
|
|
89
|
+
timeout: 120_000, // 2 minutes
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
console.log(`FirestoreStreamReadable error!\n`, {
|
|
94
|
+
table: this.table,
|
|
95
|
+
rowsRetrieved: this.rowsRetrieved,
|
|
96
|
+
}, err);
|
|
97
|
+
this.emit('error', err);
|
|
98
|
+
// clearInterval(this.maxWaitInterval)
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const rows = [];
|
|
102
|
+
let lastDocId;
|
|
103
|
+
for (const doc of qs.docs) {
|
|
104
|
+
lastDocId = doc.id;
|
|
105
|
+
rows.push({
|
|
106
|
+
id: unescapeDocId(doc.id),
|
|
107
|
+
...doc.data(),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
this.rowsRetrieved += rows.length;
|
|
111
|
+
this.logger.log(`${this.table} got ${rows.length} rows, ${this.rowsRetrieved} rowsRetrieved, totalWait: ${_ms(this.totalWait)}`);
|
|
112
|
+
this.endCursor = lastDocId;
|
|
113
|
+
this.running = false; // ready to take more _reads
|
|
114
|
+
this.lastQueryDone = Date.now();
|
|
115
|
+
for (const row of rows) {
|
|
116
|
+
this.push(row);
|
|
117
|
+
}
|
|
118
|
+
if (qs.empty || (this.originalLimit && this.rowsRetrieved >= this.originalLimit)) {
|
|
119
|
+
this.logger.log(`!!!! DONE! ${this.rowsRetrieved} rowsRetrieved, totalWait: ${_ms(this.totalWait)}`);
|
|
120
|
+
this.push(null);
|
|
121
|
+
this.done = true;
|
|
122
|
+
}
|
|
123
|
+
else if (this.opt.singleBatchBuffer) {
|
|
124
|
+
// here we don't start next query until we're asked (via next _read call)
|
|
125
|
+
// so, let's do nothing
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const rssMB = Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
129
|
+
if (rssMB <= this.opt.rssLimitMB) {
|
|
130
|
+
void this.runNextQuery();
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
this.logger.warn(`${this.table} rssLimitMB reached ${rssMB} > ${this.opt.rssLimitMB}, pausing stream`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/query.util.js
CHANGED
|
@@ -12,6 +12,8 @@ export function dbQueryToFirestoreQuery(dbQuery, emptyQuery) {
|
|
|
12
12
|
}
|
|
13
13
|
// order
|
|
14
14
|
for (const ord of dbQuery._orders) {
|
|
15
|
+
// todo: support ordering by id like this:
|
|
16
|
+
// .orderBy(FieldPath.documentId())
|
|
15
17
|
q = q.orderBy(ord.name, ord.descending ? 'desc' : 'asc');
|
|
16
18
|
}
|
|
17
19
|
// limit
|
|
@@ -21,5 +23,12 @@ export function dbQueryToFirestoreQuery(dbQuery, emptyQuery) {
|
|
|
21
23
|
// todo: check if at least id / __key__ is required to be set
|
|
22
24
|
q = q.select(...dbQuery._selectedFieldNames);
|
|
23
25
|
}
|
|
26
|
+
// cursor
|
|
27
|
+
if (dbQuery._startCursor) {
|
|
28
|
+
q = q.startAt(dbQuery._startCursor);
|
|
29
|
+
}
|
|
30
|
+
if (dbQuery._endCursor) {
|
|
31
|
+
q = q.endAt(dbQuery._endCursor);
|
|
32
|
+
}
|
|
24
33
|
return q;
|
|
25
34
|
}
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"@types/node": "^24",
|
|
13
13
|
"dotenv": "^17",
|
|
14
14
|
"firebase-admin": "^13",
|
|
15
|
-
"@naturalcycles/dev-lib": "
|
|
15
|
+
"@naturalcycles/dev-lib": "19.34.2"
|
|
16
16
|
},
|
|
17
17
|
"exports": {
|
|
18
18
|
".": "./dist/index.js"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=22.12.0"
|
|
40
40
|
},
|
|
41
|
-
"version": "2.
|
|
41
|
+
"version": "2.8.0",
|
|
42
42
|
"description": "Firestore implementation of CommonDB interface",
|
|
43
43
|
"author": "Natural Cycles Team",
|
|
44
44
|
"license": "MIT",
|
package/src/firestore.db.ts
CHANGED
|
@@ -14,7 +14,6 @@ import type {
|
|
|
14
14
|
CommonDBReadOptions,
|
|
15
15
|
CommonDBSaveMethod,
|
|
16
16
|
CommonDBSaveOptions,
|
|
17
|
-
CommonDBStreamOptions,
|
|
18
17
|
CommonDBSupport,
|
|
19
18
|
CommonDBTransactionOptions,
|
|
20
19
|
DBQuery,
|
|
@@ -26,42 +25,27 @@ import { BaseCommonDB, commonDBFullSupport } from '@naturalcycles/db-lib'
|
|
|
26
25
|
import { _isTruthy } from '@naturalcycles/js-lib'
|
|
27
26
|
import { _chunk } from '@naturalcycles/js-lib/array/array.util.js'
|
|
28
27
|
import { _assert } from '@naturalcycles/js-lib/error/assert.js'
|
|
28
|
+
import { type CommonLogger, commonLoggerMinLevel } from '@naturalcycles/js-lib/log'
|
|
29
29
|
import { _filterUndefinedValues, _omit } from '@naturalcycles/js-lib/object/object.util.js'
|
|
30
30
|
import { pMap } from '@naturalcycles/js-lib/promise/pMap.js'
|
|
31
|
-
import type { ObjectWithId, StringMap } from '@naturalcycles/js-lib/types'
|
|
31
|
+
import type { NumberOfSeconds, ObjectWithId, StringMap } from '@naturalcycles/js-lib/types'
|
|
32
32
|
import { _stringMapEntries } from '@naturalcycles/js-lib/types'
|
|
33
33
|
import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream'
|
|
34
34
|
import { escapeDocId, unescapeDocId } from './firestore.util.js'
|
|
35
|
+
import { FirestoreStreamReadable } from './firestoreStreamReadable.js'
|
|
35
36
|
import { dbQueryToFirestoreQuery } from './query.util.js'
|
|
36
37
|
|
|
37
|
-
export interface FirestoreDBCfg {
|
|
38
|
-
firestore: Firestore
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface FirestoreDBOptions extends CommonDBOptions {}
|
|
42
|
-
export interface FirestoreDBReadOptions extends CommonDBReadOptions {}
|
|
43
|
-
export interface FirestoreDBSaveOptions<ROW extends ObjectWithId>
|
|
44
|
-
extends CommonDBSaveOptions<ROW> {}
|
|
45
|
-
|
|
46
|
-
type SaveOp = 'create' | 'update' | 'set'
|
|
47
|
-
|
|
48
|
-
const methodMap: Record<CommonDBSaveMethod, SaveOp> = {
|
|
49
|
-
insert: 'create',
|
|
50
|
-
update: 'update',
|
|
51
|
-
upsert: 'set',
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export class RollbackError extends Error {
|
|
55
|
-
constructor() {
|
|
56
|
-
super('rollback')
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
38
|
export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
61
|
-
constructor(
|
|
39
|
+
constructor(cfg: FirestoreDBCfg) {
|
|
62
40
|
super()
|
|
41
|
+
this.cfg = {
|
|
42
|
+
logger: console,
|
|
43
|
+
...cfg,
|
|
44
|
+
}
|
|
63
45
|
}
|
|
64
46
|
|
|
47
|
+
cfg: FirestoreDBCfg & { logger: CommonLogger }
|
|
48
|
+
|
|
65
49
|
override support: CommonDBSupport = {
|
|
66
50
|
...commonDBFullSupport,
|
|
67
51
|
patchByQuery: false, // todo: can be implemented
|
|
@@ -76,6 +60,8 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
76
60
|
): Promise<ROW[]> {
|
|
77
61
|
if (!ids.length) return []
|
|
78
62
|
|
|
63
|
+
// todo: support PITR: https://firebase.google.com/docs/firestore/enterprise/use-pitr#read-pitr
|
|
64
|
+
|
|
79
65
|
const { firestore } = this.cfg
|
|
80
66
|
const col = firestore.collection(table)
|
|
81
67
|
|
|
@@ -95,7 +81,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
95
81
|
.filter(_isTruthy)
|
|
96
82
|
}
|
|
97
83
|
|
|
98
|
-
override async
|
|
84
|
+
override async multiGet<ROW extends ObjectWithId>(
|
|
99
85
|
map: StringMap<string[]>,
|
|
100
86
|
opt: CommonDBReadOptions = {},
|
|
101
87
|
): Promise<StringMap<ROW[]>> {
|
|
@@ -163,10 +149,24 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
163
149
|
|
|
164
150
|
override streamQuery<ROW extends ObjectWithId>(
|
|
165
151
|
q: DBQuery<ROW>,
|
|
166
|
-
|
|
152
|
+
opt_?: FirestoreDBStreamOptions,
|
|
167
153
|
): ReadableTyped<ROW> {
|
|
168
154
|
const firestoreQuery = dbQueryToFirestoreQuery(q, this.cfg.firestore.collection(q.table))
|
|
169
155
|
|
|
156
|
+
const opt: FirestoreDBStreamOptions = {
|
|
157
|
+
...this.cfg.streamOptions,
|
|
158
|
+
...opt_,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (opt.experimentalCursorStream) {
|
|
162
|
+
return new FirestoreStreamReadable(
|
|
163
|
+
firestoreQuery,
|
|
164
|
+
q,
|
|
165
|
+
opt,
|
|
166
|
+
commonLoggerMinLevel(this.cfg.logger, opt.debug ? 'log' : 'warn'),
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
170
|
return (firestoreQuery.stream() as ReadableTyped<QueryDocumentSnapshot<any>>).map(doc => {
|
|
171
171
|
return {
|
|
172
172
|
id: unescapeDocId(doc.id),
|
|
@@ -230,7 +230,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
230
230
|
)
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
override async
|
|
233
|
+
override async multiSave<ROW extends ObjectWithId>(
|
|
234
234
|
map: StringMap<ROW[]>,
|
|
235
235
|
opt: FirestoreDBSaveOptions<ROW> = {},
|
|
236
236
|
): Promise<void> {
|
|
@@ -361,7 +361,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
361
361
|
return ids.length
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
override async
|
|
364
|
+
override async multiDelete(
|
|
365
365
|
map: StringMap<string[]>,
|
|
366
366
|
opt: FirestoreDBOptions = {},
|
|
367
367
|
): Promise<number> {
|
|
@@ -394,7 +394,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
394
394
|
return refs.length
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
-
|
|
397
|
+
querySnapshotToArray<T = any>(qs: QuerySnapshot): T[] {
|
|
398
398
|
return qs.docs.map(
|
|
399
399
|
doc =>
|
|
400
400
|
({
|
|
@@ -509,3 +509,95 @@ const MAX_ITEMS = 500
|
|
|
509
509
|
const FIRESTORE_RECOMMENDED_CONCURRENCY = 8
|
|
510
510
|
|
|
511
511
|
type TableRow<ROW extends ObjectWithId> = [table: string, row: ROW]
|
|
512
|
+
|
|
513
|
+
export interface FirestoreDBCfg {
|
|
514
|
+
firestore: Firestore
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Use it to set default options to stream operations,
|
|
518
|
+
* e.g you can globally enable `experimentalCursorStream` here, set the batchSize, etc.
|
|
519
|
+
*/
|
|
520
|
+
streamOptions?: FirestoreDBStreamOptions
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Default to `console`
|
|
524
|
+
*/
|
|
525
|
+
logger?: CommonLogger
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const methodMap: Record<CommonDBSaveMethod, SaveOp> = {
|
|
529
|
+
insert: 'create',
|
|
530
|
+
update: 'update',
|
|
531
|
+
upsert: 'set',
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export class RollbackError extends Error {
|
|
535
|
+
constructor() {
|
|
536
|
+
super('rollback')
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export interface FirestoreDBStreamOptions extends FirestoreDBReadOptions {
|
|
541
|
+
/**
|
|
542
|
+
* Set to `true` to stream via experimental "cursor-query based stream".
|
|
543
|
+
*
|
|
544
|
+
* Defaults to false
|
|
545
|
+
*/
|
|
546
|
+
experimentalCursorStream?: boolean
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Applicable to `experimentalCursorStream`.
|
|
550
|
+
* Defines the size (limit) of each individual query.
|
|
551
|
+
*
|
|
552
|
+
* Default: 1000
|
|
553
|
+
*/
|
|
554
|
+
batchSize?: number
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Applicable to `experimentalCursorStream`
|
|
558
|
+
*
|
|
559
|
+
* Set to a value (number of Megabytes) to control the peak RSS size.
|
|
560
|
+
* If limit is reached - streaming will pause until the stream keeps up, and then
|
|
561
|
+
* resumes.
|
|
562
|
+
*
|
|
563
|
+
* Set to 0/undefined to disable. Stream will get "slow" then, cause it'll only run the query
|
|
564
|
+
* when _read is called.
|
|
565
|
+
*
|
|
566
|
+
* @default 1000
|
|
567
|
+
*/
|
|
568
|
+
rssLimitMB?: number
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Applicable to `experimentalCursorStream`
|
|
572
|
+
* Default false.
|
|
573
|
+
* If true, stream will pause until consumer requests more data (via _read).
|
|
574
|
+
* It means it'll run slower, as buffer will be equal to batchSize (1000) at max.
|
|
575
|
+
* There will be gaps in time between "last query loaded" and "next query requested".
|
|
576
|
+
* This mode is useful e.g for DB migrations, where you want to avoid "stale data".
|
|
577
|
+
* So, it minimizes the time between "item loaded" and "item saved" during DB migration.
|
|
578
|
+
*/
|
|
579
|
+
singleBatchBuffer?: boolean
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Set to `true` to log additional debug info, when using experimentalCursorStream.
|
|
583
|
+
*
|
|
584
|
+
* @default false
|
|
585
|
+
*/
|
|
586
|
+
debug?: boolean
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Default is undefined.
|
|
590
|
+
* If set - sets a "safety timer", which will force call _read after the specified number of seconds.
|
|
591
|
+
* This is to prevent possible "dead-lock"/race-condition that would make the stream "hang".
|
|
592
|
+
*
|
|
593
|
+
* @experimental
|
|
594
|
+
*/
|
|
595
|
+
maxWait?: NumberOfSeconds
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export interface FirestoreDBOptions extends CommonDBOptions {}
|
|
599
|
+
export interface FirestoreDBReadOptions extends CommonDBReadOptions {}
|
|
600
|
+
export interface FirestoreDBSaveOptions<ROW extends ObjectWithId>
|
|
601
|
+
extends CommonDBSaveOptions<ROW> {}
|
|
602
|
+
|
|
603
|
+
type SaveOp = 'create' | 'update' | 'set'
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Readable } from 'node:stream'
|
|
2
|
+
import { FieldPath, type Query, type QuerySnapshot } from '@google-cloud/firestore'
|
|
3
|
+
import type { DBQuery } from '@naturalcycles/db-lib'
|
|
4
|
+
import { _ms } from '@naturalcycles/js-lib/datetime/time.util.js'
|
|
5
|
+
import type { CommonLogger } from '@naturalcycles/js-lib/log'
|
|
6
|
+
import { pRetry } from '@naturalcycles/js-lib/promise/pRetry.js'
|
|
7
|
+
import type { ObjectWithId } from '@naturalcycles/js-lib/types'
|
|
8
|
+
import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream'
|
|
9
|
+
import type { FirestoreDBStreamOptions } from './firestore.db.js'
|
|
10
|
+
import { unescapeDocId } from './firestore.util.js'
|
|
11
|
+
|
|
12
|
+
export class FirestoreStreamReadable<T extends ObjectWithId = any>
|
|
13
|
+
extends Readable
|
|
14
|
+
implements ReadableTyped<T>
|
|
15
|
+
{
|
|
16
|
+
private readonly table: string
|
|
17
|
+
private readonly originalLimit: number
|
|
18
|
+
private rowsRetrieved = 0
|
|
19
|
+
private endCursor?: string
|
|
20
|
+
private running = false
|
|
21
|
+
private done = false
|
|
22
|
+
private lastQueryDone?: number
|
|
23
|
+
private totalWait = 0
|
|
24
|
+
|
|
25
|
+
private readonly opt: FirestoreDBStreamOptions & { batchSize: number; rssLimitMB: number }
|
|
26
|
+
// private readonly dsOpt: RunQueryOptions
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private q: Query,
|
|
30
|
+
dbQuery: DBQuery<T>,
|
|
31
|
+
opt: FirestoreDBStreamOptions,
|
|
32
|
+
private logger: CommonLogger,
|
|
33
|
+
) {
|
|
34
|
+
super({ objectMode: true })
|
|
35
|
+
|
|
36
|
+
this.opt = {
|
|
37
|
+
rssLimitMB: 1000,
|
|
38
|
+
batchSize: 1000,
|
|
39
|
+
...opt,
|
|
40
|
+
}
|
|
41
|
+
// todo: support PITR!
|
|
42
|
+
// this.dsOpt = {}
|
|
43
|
+
// if (opt.readAt) {
|
|
44
|
+
// // Datastore expects UnixTimestamp in milliseconds
|
|
45
|
+
// this.dsOpt.readTime = opt.readAt * 1000
|
|
46
|
+
// }
|
|
47
|
+
|
|
48
|
+
this.originalLimit = dbQuery._limitValue
|
|
49
|
+
this.table = dbQuery.table
|
|
50
|
+
|
|
51
|
+
logger.log(
|
|
52
|
+
`!! using experimentalCursorStream !! ${this.table}, batchSize: ${this.opt.batchSize}`,
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Counts how many times _read was called.
|
|
58
|
+
* For debugging.
|
|
59
|
+
*/
|
|
60
|
+
count = 0
|
|
61
|
+
|
|
62
|
+
override _read(): void {
|
|
63
|
+
// this.lastReadTimestamp = Date.now() as UnixTimestampMillis
|
|
64
|
+
|
|
65
|
+
// console.log(`_read called ${++this.count}, wasRunning: ${this.running}`) // debugging
|
|
66
|
+
this.count++
|
|
67
|
+
|
|
68
|
+
if (this.done) {
|
|
69
|
+
this.logger.warn(`!!! _read was called, but done==true`)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!this.running) {
|
|
74
|
+
void this.runNextQuery().catch(err => {
|
|
75
|
+
console.log('error in runNextQuery', err)
|
|
76
|
+
this.emit('error', err)
|
|
77
|
+
})
|
|
78
|
+
} else {
|
|
79
|
+
this.logger.log(`_read ${this.count}, wasRunning: true`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private async runNextQuery(): Promise<void> {
|
|
84
|
+
if (this.done) return
|
|
85
|
+
|
|
86
|
+
if (this.lastQueryDone) {
|
|
87
|
+
const now = Date.now()
|
|
88
|
+
this.totalWait += now - this.lastQueryDone
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.running = true
|
|
92
|
+
|
|
93
|
+
let limit = this.opt.batchSize
|
|
94
|
+
|
|
95
|
+
if (this.originalLimit) {
|
|
96
|
+
limit = Math.min(this.opt.batchSize, this.originalLimit - this.rowsRetrieved)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// console.log(`limit: ${limit}`)
|
|
100
|
+
// We have to orderBy documentId, to be able to use id as a cursor
|
|
101
|
+
let q = this.q.orderBy(FieldPath.documentId()).limit(limit)
|
|
102
|
+
if (this.endCursor) {
|
|
103
|
+
q = q.startAfter(this.endCursor)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let qs: QuerySnapshot
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await pRetry(
|
|
110
|
+
async () => {
|
|
111
|
+
qs = await q.get()
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: `FirestoreStreamReadable.query(${this.table})`,
|
|
115
|
+
maxAttempts: 5,
|
|
116
|
+
delay: 5000,
|
|
117
|
+
delayMultiplier: 2,
|
|
118
|
+
logger: this.logger,
|
|
119
|
+
timeout: 120_000, // 2 minutes
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.log(
|
|
124
|
+
`FirestoreStreamReadable error!\n`,
|
|
125
|
+
{
|
|
126
|
+
table: this.table,
|
|
127
|
+
rowsRetrieved: this.rowsRetrieved,
|
|
128
|
+
},
|
|
129
|
+
err,
|
|
130
|
+
)
|
|
131
|
+
this.emit('error', err)
|
|
132
|
+
// clearInterval(this.maxWaitInterval)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const rows: T[] = []
|
|
137
|
+
let lastDocId: string | undefined
|
|
138
|
+
|
|
139
|
+
for (const doc of qs!.docs) {
|
|
140
|
+
lastDocId = doc.id
|
|
141
|
+
rows.push({
|
|
142
|
+
id: unescapeDocId(doc.id),
|
|
143
|
+
...doc.data(),
|
|
144
|
+
} as T)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.rowsRetrieved += rows.length
|
|
148
|
+
this.logger.log(
|
|
149
|
+
`${this.table} got ${rows.length} rows, ${this.rowsRetrieved} rowsRetrieved, totalWait: ${_ms(
|
|
150
|
+
this.totalWait,
|
|
151
|
+
)}`,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
this.endCursor = lastDocId
|
|
155
|
+
this.running = false // ready to take more _reads
|
|
156
|
+
this.lastQueryDone = Date.now()
|
|
157
|
+
|
|
158
|
+
for (const row of rows) {
|
|
159
|
+
this.push(row)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (qs!.empty || (this.originalLimit && this.rowsRetrieved >= this.originalLimit)) {
|
|
163
|
+
this.logger.log(
|
|
164
|
+
`!!!! DONE! ${this.rowsRetrieved} rowsRetrieved, totalWait: ${_ms(this.totalWait)}`,
|
|
165
|
+
)
|
|
166
|
+
this.push(null)
|
|
167
|
+
this.done = true
|
|
168
|
+
} else if (this.opt.singleBatchBuffer) {
|
|
169
|
+
// here we don't start next query until we're asked (via next _read call)
|
|
170
|
+
// so, let's do nothing
|
|
171
|
+
} else {
|
|
172
|
+
const rssMB = Math.round(process.memoryUsage().rss / 1024 / 1024)
|
|
173
|
+
|
|
174
|
+
if (rssMB <= this.opt.rssLimitMB) {
|
|
175
|
+
void this.runNextQuery()
|
|
176
|
+
} else {
|
|
177
|
+
this.logger.warn(
|
|
178
|
+
`${this.table} rssLimitMB reached ${rssMB} > ${this.opt.rssLimitMB}, pausing stream`,
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
package/src/index.ts
CHANGED
package/src/query.util.ts
CHANGED
|
@@ -22,6 +22,8 @@ export function dbQueryToFirestoreQuery<ROW extends ObjectWithId>(
|
|
|
22
22
|
|
|
23
23
|
// order
|
|
24
24
|
for (const ord of dbQuery._orders) {
|
|
25
|
+
// todo: support ordering by id like this:
|
|
26
|
+
// .orderBy(FieldPath.documentId())
|
|
25
27
|
q = q.orderBy(ord.name as string, ord.descending ? 'desc' : 'asc')
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -34,5 +36,14 @@ export function dbQueryToFirestoreQuery<ROW extends ObjectWithId>(
|
|
|
34
36
|
q = q.select(...(dbQuery._selectedFieldNames as string[]))
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
// cursor
|
|
40
|
+
if (dbQuery._startCursor) {
|
|
41
|
+
q = q.startAt(dbQuery._startCursor)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (dbQuery._endCursor) {
|
|
45
|
+
q = q.endAt(dbQuery._endCursor)
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
return q
|
|
38
49
|
}
|