@naturalcycles/firestore-lib 2.5.0 → 2.7.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 +8 -2
- package/dist/firestore.db.js +93 -3
- package/package.json +1 -1
- package/src/firestore.db.ts +149 -6
package/dist/firestore.db.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Firestore, Query, Transaction } from '@google-cloud/firestore';
|
|
2
|
-
import type { CommonDB, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, CommonDBSupport, CommonDBTransactionOptions, DBQuery, DBTransaction, DBTransactionFn, RunQueryResult } from '@naturalcycles/db-lib';
|
|
2
|
+
import type { CommonDB, CommonDBOptions, CommonDBReadOptions, CommonDBSaveOptions, CommonDBStreamOptions, CommonDBSupport, CommonDBTransactionOptions, DBQuery, DBTransaction, DBTransactionFn, RunQueryResult } from '@naturalcycles/db-lib';
|
|
3
3
|
import { BaseCommonDB } from '@naturalcycles/db-lib';
|
|
4
4
|
import type { ObjectWithId, StringMap } from '@naturalcycles/js-lib/types';
|
|
5
5
|
import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream';
|
|
@@ -8,6 +8,8 @@ export interface FirestoreDBCfg {
|
|
|
8
8
|
}
|
|
9
9
|
export interface FirestoreDBOptions extends CommonDBOptions {
|
|
10
10
|
}
|
|
11
|
+
export interface FirestoreDBReadOptions extends CommonDBReadOptions {
|
|
12
|
+
}
|
|
11
13
|
export interface FirestoreDBSaveOptions<ROW extends ObjectWithId> extends CommonDBSaveOptions<ROW> {
|
|
12
14
|
}
|
|
13
15
|
export declare class RollbackError extends Error {
|
|
@@ -17,14 +19,18 @@ export declare class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
17
19
|
cfg: FirestoreDBCfg;
|
|
18
20
|
constructor(cfg: FirestoreDBCfg);
|
|
19
21
|
support: CommonDBSupport;
|
|
20
|
-
getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?:
|
|
22
|
+
getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: FirestoreDBReadOptions): Promise<ROW[]>;
|
|
23
|
+
multiGet<ROW extends ObjectWithId>(map: StringMap<string[]>, opt?: CommonDBReadOptions): Promise<StringMap<ROW[]>>;
|
|
21
24
|
runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: FirestoreDBOptions): Promise<RunQueryResult<ROW>>;
|
|
22
25
|
runFirestoreQuery<ROW extends ObjectWithId>(q: Query): Promise<ROW[]>;
|
|
23
26
|
runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: FirestoreDBOptions): Promise<number>;
|
|
24
27
|
streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBStreamOptions): ReadableTyped<ROW>;
|
|
25
28
|
saveBatch<ROW extends ObjectWithId>(table: string, rows: ROW[], opt?: FirestoreDBSaveOptions<ROW>): Promise<void>;
|
|
29
|
+
multiSave<ROW extends ObjectWithId>(map: StringMap<ROW[]>, opt?: FirestoreDBSaveOptions<ROW>): Promise<void>;
|
|
30
|
+
patchById<ROW extends ObjectWithId>(table: string, id: string, patch: Partial<ROW>, opt?: FirestoreDBOptions): Promise<void>;
|
|
26
31
|
deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: FirestoreDBOptions): Promise<number>;
|
|
27
32
|
deleteByIds(table: string, ids: string[], opt?: FirestoreDBOptions): Promise<number>;
|
|
33
|
+
multiDelete(map: StringMap<string[]>, opt?: FirestoreDBOptions): Promise<number>;
|
|
28
34
|
private querySnapshotToArray;
|
|
29
35
|
runInTransaction(fn: DBTransactionFn, opt?: CommonDBTransactionOptions): Promise<void>;
|
|
30
36
|
/**
|
package/dist/firestore.db.js
CHANGED
|
@@ -47,6 +47,29 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
47
47
|
})
|
|
48
48
|
.filter(_isTruthy);
|
|
49
49
|
}
|
|
50
|
+
async multiGet(map, opt = {}) {
|
|
51
|
+
const result = {};
|
|
52
|
+
const { firestore } = this.cfg;
|
|
53
|
+
const refs = [];
|
|
54
|
+
for (const [table, ids] of _stringMapEntries(map)) {
|
|
55
|
+
result[table] = [];
|
|
56
|
+
const col = firestore.collection(table);
|
|
57
|
+
refs.push(...ids.map(id => col.doc(escapeDocId(id))));
|
|
58
|
+
}
|
|
59
|
+
const snapshots = await (opt.tx?.tx || firestore).getAll(...refs);
|
|
60
|
+
snapshots.forEach(snap => {
|
|
61
|
+
const data = snap.data();
|
|
62
|
+
if (data === undefined)
|
|
63
|
+
return;
|
|
64
|
+
const table = snap.ref.parent.id;
|
|
65
|
+
const row = {
|
|
66
|
+
id: unescapeDocId(snap.id),
|
|
67
|
+
...data,
|
|
68
|
+
};
|
|
69
|
+
result[table].push(row);
|
|
70
|
+
});
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
50
73
|
// QUERY
|
|
51
74
|
async runQuery(q, opt) {
|
|
52
75
|
const idFilter = q._filters.find(f => f.name === 'id');
|
|
@@ -96,6 +119,10 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
96
119
|
return;
|
|
97
120
|
}
|
|
98
121
|
await pMap(_chunk(rows, MAX_ITEMS), async (chunk) => {
|
|
122
|
+
// .batch is called "Atomic batch writer"
|
|
123
|
+
// Executes multiple writes in a single atomic transaction-like commit — all succeed or all fail.
|
|
124
|
+
// If any write in the batch fails (e.g., permission error, missing doc), the whole batch fails.
|
|
125
|
+
// Good for small, related sets of writes where consistency is critical.
|
|
99
126
|
const batch = firestore.batch();
|
|
100
127
|
for (const row of chunk) {
|
|
101
128
|
_assert(row.id, `firestore-db doesn't support id auto-generation, but empty id was provided in saveBatch`);
|
|
@@ -105,6 +132,47 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
105
132
|
await batch.commit();
|
|
106
133
|
}, { concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY });
|
|
107
134
|
}
|
|
135
|
+
async multiSave(map, opt = {}) {
|
|
136
|
+
const { firestore } = this.cfg;
|
|
137
|
+
const method = methodMap[opt.saveMethod] || 'set';
|
|
138
|
+
if (opt.tx) {
|
|
139
|
+
const { tx } = opt.tx;
|
|
140
|
+
for (const [table, rows] of _stringMapEntries(map)) {
|
|
141
|
+
const col = firestore.collection(table);
|
|
142
|
+
for (const row of rows) {
|
|
143
|
+
_assert(row.id, `firestore-db doesn't support id auto-generation, but empty id was provided in multiSaveBatch`);
|
|
144
|
+
const { id, ...rowWithoutId } = row;
|
|
145
|
+
tx[method](col.doc(escapeDocId(id)), _filterUndefinedValues(rowWithoutId));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const tableRows = [];
|
|
151
|
+
for (const [table, rows] of _stringMapEntries(map)) {
|
|
152
|
+
for (const row of rows) {
|
|
153
|
+
tableRows.push([table, row]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
await pMap(_chunk(tableRows, MAX_ITEMS), async (chunk) => {
|
|
157
|
+
const batch = firestore.batch();
|
|
158
|
+
for (const [table, row] of chunk) {
|
|
159
|
+
_assert(row.id, `firestore-db doesn't support id auto-generation, but empty id was provided in multiSaveBatch`);
|
|
160
|
+
const { id, ...rowWithoutId } = row;
|
|
161
|
+
batch[method](firestore.collection(table).doc(escapeDocId(id)), _filterUndefinedValues(rowWithoutId));
|
|
162
|
+
}
|
|
163
|
+
await batch.commit();
|
|
164
|
+
}, { concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY });
|
|
165
|
+
}
|
|
166
|
+
async patchById(table, id, patch, opt = {}) {
|
|
167
|
+
const { firestore } = this.cfg;
|
|
168
|
+
const col = firestore.collection(table);
|
|
169
|
+
if (opt.tx) {
|
|
170
|
+
const { tx } = opt.tx;
|
|
171
|
+
tx.update(col.doc(escapeDocId(id)), patch);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
await col.doc(escapeDocId(id)).update(patch);
|
|
175
|
+
}
|
|
108
176
|
// DELETE
|
|
109
177
|
async deleteByQuery(q, opt) {
|
|
110
178
|
let ids;
|
|
@@ -135,11 +203,33 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
135
203
|
batch.delete(col.doc(escapeDocId(id)));
|
|
136
204
|
}
|
|
137
205
|
await batch.commit();
|
|
138
|
-
}, {
|
|
139
|
-
concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY,
|
|
140
|
-
});
|
|
206
|
+
}, { concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY });
|
|
141
207
|
return ids.length;
|
|
142
208
|
}
|
|
209
|
+
async multiDelete(map, opt = {}) {
|
|
210
|
+
const { firestore } = this.cfg;
|
|
211
|
+
const refs = [];
|
|
212
|
+
for (const [table, ids] of _stringMapEntries(map)) {
|
|
213
|
+
const col = firestore.collection(table);
|
|
214
|
+
refs.push(...ids.map(id => col.doc(escapeDocId(id))));
|
|
215
|
+
}
|
|
216
|
+
if (opt.tx) {
|
|
217
|
+
const { tx } = opt.tx;
|
|
218
|
+
for (const ref of refs) {
|
|
219
|
+
tx.delete(ref);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
await pMap(_chunk(refs, MAX_ITEMS), async (chunk) => {
|
|
224
|
+
const batch = firestore.batch();
|
|
225
|
+
for (const ref of chunk) {
|
|
226
|
+
batch.delete(ref);
|
|
227
|
+
}
|
|
228
|
+
await batch.commit();
|
|
229
|
+
}, { concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY });
|
|
230
|
+
}
|
|
231
|
+
return refs.length;
|
|
232
|
+
}
|
|
143
233
|
querySnapshotToArray(qs) {
|
|
144
234
|
return qs.docs.map(doc => ({
|
|
145
235
|
id: unescapeDocId(doc.id),
|
package/package.json
CHANGED
package/src/firestore.db.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
DocumentReference,
|
|
2
3
|
Firestore,
|
|
3
4
|
Query,
|
|
4
5
|
QueryDocumentSnapshot,
|
|
5
6
|
QuerySnapshot,
|
|
6
7
|
Transaction,
|
|
8
|
+
UpdateData,
|
|
7
9
|
} from '@google-cloud/firestore'
|
|
8
10
|
import { FieldValue } from '@google-cloud/firestore'
|
|
9
11
|
import type {
|
|
10
12
|
CommonDB,
|
|
11
13
|
CommonDBOptions,
|
|
14
|
+
CommonDBReadOptions,
|
|
12
15
|
CommonDBSaveMethod,
|
|
13
16
|
CommonDBSaveOptions,
|
|
14
17
|
CommonDBStreamOptions,
|
|
@@ -36,6 +39,7 @@ export interface FirestoreDBCfg {
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export interface FirestoreDBOptions extends CommonDBOptions {}
|
|
42
|
+
export interface FirestoreDBReadOptions extends CommonDBReadOptions {}
|
|
39
43
|
export interface FirestoreDBSaveOptions<ROW extends ObjectWithId>
|
|
40
44
|
extends CommonDBSaveOptions<ROW> {}
|
|
41
45
|
|
|
@@ -68,7 +72,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
68
72
|
override async getByIds<ROW extends ObjectWithId>(
|
|
69
73
|
table: string,
|
|
70
74
|
ids: string[],
|
|
71
|
-
opt:
|
|
75
|
+
opt: FirestoreDBReadOptions = {},
|
|
72
76
|
): Promise<ROW[]> {
|
|
73
77
|
if (!ids.length) return []
|
|
74
78
|
|
|
@@ -91,6 +95,34 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
91
95
|
.filter(_isTruthy)
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
override async multiGet<ROW extends ObjectWithId>(
|
|
99
|
+
map: StringMap<string[]>,
|
|
100
|
+
opt: CommonDBReadOptions = {},
|
|
101
|
+
): Promise<StringMap<ROW[]>> {
|
|
102
|
+
const result: StringMap<ROW[]> = {}
|
|
103
|
+
const { firestore } = this.cfg
|
|
104
|
+
const refs: DocumentReference[] = []
|
|
105
|
+
for (const [table, ids] of _stringMapEntries(map)) {
|
|
106
|
+
result[table] = []
|
|
107
|
+
const col = firestore.collection(table)
|
|
108
|
+
refs.push(...ids.map(id => col.doc(escapeDocId(id))))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const snapshots = await ((opt.tx as FirestoreDBTransaction)?.tx || firestore).getAll(...refs)
|
|
112
|
+
snapshots.forEach(snap => {
|
|
113
|
+
const data = snap.data()
|
|
114
|
+
if (data === undefined) return
|
|
115
|
+
const table = snap.ref.parent.id
|
|
116
|
+
const row = {
|
|
117
|
+
id: unescapeDocId(snap.id),
|
|
118
|
+
...data,
|
|
119
|
+
} as ROW
|
|
120
|
+
result[table]!.push(row)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
return result
|
|
124
|
+
}
|
|
125
|
+
|
|
94
126
|
// QUERY
|
|
95
127
|
override async runQuery<ROW extends ObjectWithId>(
|
|
96
128
|
q: DBQuery<ROW>,
|
|
@@ -174,6 +206,10 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
174
206
|
await pMap(
|
|
175
207
|
_chunk(rows, MAX_ITEMS),
|
|
176
208
|
async chunk => {
|
|
209
|
+
// .batch is called "Atomic batch writer"
|
|
210
|
+
// Executes multiple writes in a single atomic transaction-like commit — all succeed or all fail.
|
|
211
|
+
// If any write in the batch fails (e.g., permission error, missing doc), the whole batch fails.
|
|
212
|
+
// Good for small, related sets of writes where consistency is critical.
|
|
177
213
|
const batch = firestore.batch()
|
|
178
214
|
|
|
179
215
|
for (const row of chunk) {
|
|
@@ -194,6 +230,82 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
194
230
|
)
|
|
195
231
|
}
|
|
196
232
|
|
|
233
|
+
override async multiSave<ROW extends ObjectWithId>(
|
|
234
|
+
map: StringMap<ROW[]>,
|
|
235
|
+
opt: FirestoreDBSaveOptions<ROW> = {},
|
|
236
|
+
): Promise<void> {
|
|
237
|
+
const { firestore } = this.cfg
|
|
238
|
+
const method: SaveOp = methodMap[opt.saveMethod!] || 'set'
|
|
239
|
+
|
|
240
|
+
if (opt.tx) {
|
|
241
|
+
const { tx } = opt.tx as FirestoreDBTransaction
|
|
242
|
+
|
|
243
|
+
for (const [table, rows] of _stringMapEntries(map)) {
|
|
244
|
+
const col = firestore.collection(table)
|
|
245
|
+
for (const row of rows) {
|
|
246
|
+
_assert(
|
|
247
|
+
row.id,
|
|
248
|
+
`firestore-db doesn't support id auto-generation, but empty id was provided in multiSaveBatch`,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
const { id, ...rowWithoutId } = row
|
|
252
|
+
tx[method as 'set' | 'create'](
|
|
253
|
+
col.doc(escapeDocId(id)),
|
|
254
|
+
_filterUndefinedValues(rowWithoutId),
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const tableRows: TableRow<ROW>[] = []
|
|
262
|
+
for (const [table, rows] of _stringMapEntries(map)) {
|
|
263
|
+
for (const row of rows) {
|
|
264
|
+
tableRows.push([table, row])
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await pMap(
|
|
269
|
+
_chunk(tableRows, MAX_ITEMS),
|
|
270
|
+
async chunk => {
|
|
271
|
+
const batch = firestore.batch()
|
|
272
|
+
|
|
273
|
+
for (const [table, row] of chunk) {
|
|
274
|
+
_assert(
|
|
275
|
+
row.id,
|
|
276
|
+
`firestore-db doesn't support id auto-generation, but empty id was provided in multiSaveBatch`,
|
|
277
|
+
)
|
|
278
|
+
const { id, ...rowWithoutId } = row
|
|
279
|
+
batch[method as 'set' | 'create'](
|
|
280
|
+
firestore.collection(table).doc(escapeDocId(id)),
|
|
281
|
+
_filterUndefinedValues(rowWithoutId),
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
await batch.commit()
|
|
286
|
+
},
|
|
287
|
+
{ concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY },
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
override async patchById<ROW extends ObjectWithId>(
|
|
292
|
+
table: string,
|
|
293
|
+
id: string,
|
|
294
|
+
patch: Partial<ROW>,
|
|
295
|
+
opt: FirestoreDBOptions = {},
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
const { firestore } = this.cfg
|
|
298
|
+
const col = firestore.collection(table)
|
|
299
|
+
|
|
300
|
+
if (opt.tx) {
|
|
301
|
+
const { tx } = opt.tx as FirestoreDBTransaction
|
|
302
|
+
tx.update(col.doc(escapeDocId(id)), patch as UpdateData<ROW>)
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await col.doc(escapeDocId(id)).update(patch)
|
|
307
|
+
}
|
|
308
|
+
|
|
197
309
|
// DELETE
|
|
198
310
|
override async deleteByQuery<ROW extends ObjectWithId>(
|
|
199
311
|
q: DBQuery<ROW>,
|
|
@@ -238,21 +350,50 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
238
350
|
_chunk(ids, MAX_ITEMS),
|
|
239
351
|
async chunk => {
|
|
240
352
|
const batch = firestore.batch()
|
|
241
|
-
|
|
242
353
|
for (const id of chunk) {
|
|
243
354
|
batch.delete(col.doc(escapeDocId(id)))
|
|
244
355
|
}
|
|
245
|
-
|
|
246
356
|
await batch.commit()
|
|
247
357
|
},
|
|
248
|
-
{
|
|
249
|
-
concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY,
|
|
250
|
-
},
|
|
358
|
+
{ concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY },
|
|
251
359
|
)
|
|
252
360
|
|
|
253
361
|
return ids.length
|
|
254
362
|
}
|
|
255
363
|
|
|
364
|
+
override async multiDelete(
|
|
365
|
+
map: StringMap<string[]>,
|
|
366
|
+
opt: FirestoreDBOptions = {},
|
|
367
|
+
): Promise<number> {
|
|
368
|
+
const { firestore } = this.cfg
|
|
369
|
+
const refs: DocumentReference[] = []
|
|
370
|
+
for (const [table, ids] of _stringMapEntries(map)) {
|
|
371
|
+
const col = firestore.collection(table)
|
|
372
|
+
refs.push(...ids.map(id => col.doc(escapeDocId(id))))
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (opt.tx) {
|
|
376
|
+
const { tx } = opt.tx as FirestoreDBTransaction
|
|
377
|
+
for (const ref of refs) {
|
|
378
|
+
tx.delete(ref)
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
await pMap(
|
|
382
|
+
_chunk(refs, MAX_ITEMS),
|
|
383
|
+
async chunk => {
|
|
384
|
+
const batch = firestore.batch()
|
|
385
|
+
for (const ref of chunk) {
|
|
386
|
+
batch.delete(ref)
|
|
387
|
+
}
|
|
388
|
+
await batch.commit()
|
|
389
|
+
},
|
|
390
|
+
{ concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY },
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return refs.length
|
|
395
|
+
}
|
|
396
|
+
|
|
256
397
|
private querySnapshotToArray<T = any>(qs: QuerySnapshot): T[] {
|
|
257
398
|
return qs.docs.map(
|
|
258
399
|
doc =>
|
|
@@ -366,3 +507,5 @@ export class FirestoreDBTransaction implements DBTransaction {
|
|
|
366
507
|
const MAX_ITEMS = 500
|
|
367
508
|
// It's an empyrical value, but anything less than infinity is better than infinity
|
|
368
509
|
const FIRESTORE_RECOMMENDED_CONCURRENCY = 8
|
|
510
|
+
|
|
511
|
+
type TableRow<ROW extends ObjectWithId> = [table: string, row: ROW]
|