@naturalcycles/firestore-lib 2.4.0 → 2.6.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 +9 -3
- package/dist/firestore.db.js +110 -18
- package/dist/query.util.js +7 -8
- package/package.json +1 -1
- package/src/firestore.db.ts +185 -34
- package/src/query.util.ts +8 -8
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
|
+
multiGetByIds<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
|
-
runFirestoreQuery<ROW extends ObjectWithId>(q: Query
|
|
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
|
+
multiSaveBatch<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
|
+
multiDeleteByIds(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 multiGetByIds(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');
|
|
@@ -57,14 +80,14 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
57
80
|
};
|
|
58
81
|
}
|
|
59
82
|
const firestoreQuery = dbQueryToFirestoreQuery(q, this.cfg.firestore.collection(q.table));
|
|
60
|
-
let rows = await this.runFirestoreQuery(firestoreQuery
|
|
83
|
+
let rows = await this.runFirestoreQuery(firestoreQuery);
|
|
61
84
|
// Special case when projection query didn't specify 'id'
|
|
62
85
|
if (q._selectedFieldNames && !q._selectedFieldNames.includes('id')) {
|
|
63
86
|
rows = rows.map(r => _omit(r, ['id']));
|
|
64
87
|
}
|
|
65
88
|
return { rows };
|
|
66
89
|
}
|
|
67
|
-
async runFirestoreQuery(q
|
|
90
|
+
async runFirestoreQuery(q) {
|
|
68
91
|
return this.querySnapshotToArray(await q.get());
|
|
69
92
|
}
|
|
70
93
|
async runQueryCount(q, _opt) {
|
|
@@ -90,19 +113,65 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
90
113
|
const { tx } = opt.tx;
|
|
91
114
|
for (const row of rows) {
|
|
92
115
|
_assert(row.id, `firestore-db doesn't support id auto-generation, but empty id was provided in saveBatch`);
|
|
93
|
-
|
|
116
|
+
const { id, ...rowWithoutId } = row;
|
|
117
|
+
tx[method](col.doc(escapeDocId(id)), _filterUndefinedValues(rowWithoutId));
|
|
94
118
|
}
|
|
95
119
|
return;
|
|
96
120
|
}
|
|
97
|
-
|
|
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`);
|
|
102
|
-
|
|
129
|
+
const { id, ...rowWithoutId } = row;
|
|
130
|
+
batch[method](col.doc(escapeDocId(id)), _filterUndefinedValues(rowWithoutId));
|
|
131
|
+
}
|
|
132
|
+
await batch.commit();
|
|
133
|
+
}, { concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY });
|
|
134
|
+
}
|
|
135
|
+
async multiSaveBatch(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));
|
|
103
162
|
}
|
|
104
163
|
await batch.commit();
|
|
105
|
-
}, { concurrency:
|
|
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);
|
|
106
175
|
}
|
|
107
176
|
// DELETE
|
|
108
177
|
async deleteByQuery(q, opt) {
|
|
@@ -128,24 +197,44 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
128
197
|
}
|
|
129
198
|
return ids.length;
|
|
130
199
|
}
|
|
131
|
-
await pMap(_chunk(ids,
|
|
200
|
+
await pMap(_chunk(ids, MAX_ITEMS), async (chunk) => {
|
|
132
201
|
const batch = firestore.batch();
|
|
133
202
|
for (const id of chunk) {
|
|
134
203
|
batch.delete(col.doc(escapeDocId(id)));
|
|
135
204
|
}
|
|
136
205
|
await batch.commit();
|
|
137
|
-
});
|
|
206
|
+
}, { concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY });
|
|
138
207
|
return ids.length;
|
|
139
208
|
}
|
|
209
|
+
async multiDeleteByIds(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
|
+
}
|
|
140
233
|
querySnapshotToArray(qs) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
...doc.data(),
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
return rows;
|
|
234
|
+
return qs.docs.map(doc => ({
|
|
235
|
+
id: unescapeDocId(doc.id),
|
|
236
|
+
...doc.data(),
|
|
237
|
+
}));
|
|
149
238
|
}
|
|
150
239
|
async runInTransaction(fn, opt = {}) {
|
|
151
240
|
const { readOnly } = opt;
|
|
@@ -174,7 +263,6 @@ export class FirestoreDB extends BaseCommonDB {
|
|
|
174
263
|
const batch = firestore.batch();
|
|
175
264
|
for (const [id, increment] of _stringMapEntries(incrementMap)) {
|
|
176
265
|
batch.set(col.doc(escapeDocId(id)), {
|
|
177
|
-
// todo: lazy-load FieldValue
|
|
178
266
|
[prop]: FieldValue.increment(increment),
|
|
179
267
|
}, { merge: true });
|
|
180
268
|
}
|
|
@@ -214,3 +302,7 @@ export class FirestoreDBTransaction {
|
|
|
214
302
|
return await this.db.deleteByIds(table, ids, { ...opt, tx: this });
|
|
215
303
|
}
|
|
216
304
|
}
|
|
305
|
+
// Datastore (also Firestore and other Google APIs) supports max 500 of items when saving/deleting, etc.
|
|
306
|
+
const MAX_ITEMS = 500;
|
|
307
|
+
// It's an empyrical value, but anything less than infinity is better than infinity
|
|
308
|
+
const FIRESTORE_RECOMMENDED_CONCURRENCY = 8;
|
package/dist/query.util.js
CHANGED
|
@@ -5,16 +5,15 @@ const OP_MAP = {
|
|
|
5
5
|
// in: 'array-contains',
|
|
6
6
|
};
|
|
7
7
|
export function dbQueryToFirestoreQuery(dbQuery, emptyQuery) {
|
|
8
|
+
let q = emptyQuery;
|
|
8
9
|
// filter
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}, emptyQuery);
|
|
10
|
+
for (const f of dbQuery._filters) {
|
|
11
|
+
q = q.where(f.name, OP_MAP[f.op] || f.op, f.val);
|
|
12
|
+
}
|
|
13
13
|
// order
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}, q);
|
|
14
|
+
for (const ord of dbQuery._orders) {
|
|
15
|
+
q = q.orderBy(ord.name, ord.descending ? 'desc' : 'asc');
|
|
16
|
+
}
|
|
18
17
|
// limit
|
|
19
18
|
q = q.limit(dbQuery._limitValue);
|
|
20
19
|
// selectedFields
|
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
|
|
|
@@ -85,12 +89,40 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
85
89
|
if (data === undefined) return
|
|
86
90
|
return {
|
|
87
91
|
id: unescapeDocId(doc.id),
|
|
88
|
-
...
|
|
89
|
-
}
|
|
92
|
+
...data,
|
|
93
|
+
} as ROW
|
|
90
94
|
})
|
|
91
95
|
.filter(_isTruthy)
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
override async multiGetByIds<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>,
|
|
@@ -106,7 +138,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
106
138
|
|
|
107
139
|
const firestoreQuery = dbQueryToFirestoreQuery(q, this.cfg.firestore.collection(q.table))
|
|
108
140
|
|
|
109
|
-
let rows = await this.runFirestoreQuery<ROW>(firestoreQuery
|
|
141
|
+
let rows = await this.runFirestoreQuery<ROW>(firestoreQuery)
|
|
110
142
|
|
|
111
143
|
// Special case when projection query didn't specify 'id'
|
|
112
144
|
if (q._selectedFieldNames && !q._selectedFieldNames.includes('id')) {
|
|
@@ -116,10 +148,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
116
148
|
return { rows }
|
|
117
149
|
}
|
|
118
150
|
|
|
119
|
-
async runFirestoreQuery<ROW extends ObjectWithId>(
|
|
120
|
-
q: Query,
|
|
121
|
-
_opt?: FirestoreDBOptions,
|
|
122
|
-
): Promise<ROW[]> {
|
|
151
|
+
async runFirestoreQuery<ROW extends ObjectWithId>(q: Query): Promise<ROW[]> {
|
|
123
152
|
return this.querySnapshotToArray(await q.get())
|
|
124
153
|
}
|
|
125
154
|
|
|
@@ -165,15 +194,22 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
165
194
|
`firestore-db doesn't support id auto-generation, but empty id was provided in saveBatch`,
|
|
166
195
|
)
|
|
167
196
|
|
|
168
|
-
|
|
197
|
+
const { id, ...rowWithoutId } = row
|
|
198
|
+
tx[method as 'set' | 'create'](
|
|
199
|
+
col.doc(escapeDocId(id)),
|
|
200
|
+
_filterUndefinedValues(rowWithoutId),
|
|
201
|
+
)
|
|
169
202
|
}
|
|
170
203
|
return
|
|
171
204
|
}
|
|
172
205
|
|
|
173
|
-
// Firestore allows max 500 items in one batch
|
|
174
206
|
await pMap(
|
|
175
|
-
_chunk(rows,
|
|
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) {
|
|
@@ -181,18 +217,95 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
181
217
|
row.id,
|
|
182
218
|
`firestore-db doesn't support id auto-generation, but empty id was provided in saveBatch`,
|
|
183
219
|
)
|
|
220
|
+
const { id, ...rowWithoutId } = row
|
|
221
|
+
batch[method as 'set' | 'create'](
|
|
222
|
+
col.doc(escapeDocId(id)),
|
|
223
|
+
_filterUndefinedValues(rowWithoutId),
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await batch.commit()
|
|
228
|
+
},
|
|
229
|
+
{ concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY },
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
override async multiSaveBatch<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
|
|
184
279
|
batch[method as 'set' | 'create'](
|
|
185
|
-
|
|
186
|
-
_filterUndefinedValues(
|
|
280
|
+
firestore.collection(table).doc(escapeDocId(id)),
|
|
281
|
+
_filterUndefinedValues(rowWithoutId),
|
|
187
282
|
)
|
|
188
283
|
}
|
|
189
284
|
|
|
190
285
|
await batch.commit()
|
|
191
286
|
},
|
|
192
|
-
{ concurrency:
|
|
287
|
+
{ concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY },
|
|
193
288
|
)
|
|
194
289
|
}
|
|
195
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
|
+
|
|
196
309
|
// DELETE
|
|
197
310
|
override async deleteByQuery<ROW extends ObjectWithId>(
|
|
198
311
|
q: DBQuery<ROW>,
|
|
@@ -208,7 +321,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
208
321
|
q.select([]),
|
|
209
322
|
this.cfg.firestore.collection(q.table),
|
|
210
323
|
)
|
|
211
|
-
ids = (await this.runFirestoreQuery<
|
|
324
|
+
ids = (await this.runFirestoreQuery<ObjectWithId>(firestoreQuery)).map(obj => obj.id)
|
|
212
325
|
}
|
|
213
326
|
|
|
214
327
|
await this.deleteByIds(q.table, ids, opt)
|
|
@@ -233,30 +346,62 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
233
346
|
return ids.length
|
|
234
347
|
}
|
|
235
348
|
|
|
236
|
-
await pMap(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
349
|
+
await pMap(
|
|
350
|
+
_chunk(ids, MAX_ITEMS),
|
|
351
|
+
async chunk => {
|
|
352
|
+
const batch = firestore.batch()
|
|
353
|
+
for (const id of chunk) {
|
|
354
|
+
batch.delete(col.doc(escapeDocId(id)))
|
|
355
|
+
}
|
|
356
|
+
await batch.commit()
|
|
357
|
+
},
|
|
358
|
+
{ concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY },
|
|
359
|
+
)
|
|
245
360
|
|
|
246
361
|
return ids.length
|
|
247
362
|
}
|
|
248
363
|
|
|
249
|
-
|
|
250
|
-
|
|
364
|
+
override async multiDeleteByIds(
|
|
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
|
+
}
|
|
251
374
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
}
|
|
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
|
+
}
|
|
258
396
|
|
|
259
|
-
|
|
397
|
+
private querySnapshotToArray<T = any>(qs: QuerySnapshot): T[] {
|
|
398
|
+
return qs.docs.map(
|
|
399
|
+
doc =>
|
|
400
|
+
({
|
|
401
|
+
id: unescapeDocId(doc.id),
|
|
402
|
+
...doc.data(),
|
|
403
|
+
}) as T,
|
|
404
|
+
)
|
|
260
405
|
}
|
|
261
406
|
|
|
262
407
|
override async runInTransaction(
|
|
@@ -301,7 +446,6 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
|
|
|
301
446
|
batch.set(
|
|
302
447
|
col.doc(escapeDocId(id)),
|
|
303
448
|
{
|
|
304
|
-
// todo: lazy-load FieldValue
|
|
305
449
|
[prop]: FieldValue.increment(increment),
|
|
306
450
|
},
|
|
307
451
|
{ merge: true },
|
|
@@ -358,3 +502,10 @@ export class FirestoreDBTransaction implements DBTransaction {
|
|
|
358
502
|
return await this.db.deleteByIds(table, ids, { ...opt, tx: this })
|
|
359
503
|
}
|
|
360
504
|
}
|
|
505
|
+
|
|
506
|
+
// Datastore (also Firestore and other Google APIs) supports max 500 of items when saving/deleting, etc.
|
|
507
|
+
const MAX_ITEMS = 500
|
|
508
|
+
// It's an empyrical value, but anything less than infinity is better than infinity
|
|
509
|
+
const FIRESTORE_RECOMMENDED_CONCURRENCY = 8
|
|
510
|
+
|
|
511
|
+
type TableRow<ROW extends ObjectWithId> = [table: string, row: ROW]
|
package/src/query.util.ts
CHANGED
|
@@ -13,17 +13,17 @@ export function dbQueryToFirestoreQuery<ROW extends ObjectWithId>(
|
|
|
13
13
|
dbQuery: DBQuery<ROW>,
|
|
14
14
|
emptyQuery: Query,
|
|
15
15
|
): Query {
|
|
16
|
+
let q = emptyQuery
|
|
17
|
+
|
|
16
18
|
// filter
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}, emptyQuery)
|
|
19
|
+
for (const f of dbQuery._filters) {
|
|
20
|
+
q = q.where(f.name as string, OP_MAP[f.op] || (f.op as WhereFilterOp), f.val)
|
|
21
|
+
}
|
|
21
22
|
|
|
22
23
|
// order
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}, q)
|
|
24
|
+
for (const ord of dbQuery._orders) {
|
|
25
|
+
q = q.orderBy(ord.name as string, ord.descending ? 'desc' : 'asc')
|
|
26
|
+
}
|
|
27
27
|
|
|
28
28
|
// limit
|
|
29
29
|
q = q.limit(dbQuery._limitValue)
|