@naturalcycles/firestore-lib 2.3.0 → 2.5.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.
@@ -19,7 +19,7 @@ export declare class FirestoreDB extends BaseCommonDB implements CommonDB {
19
19
  support: CommonDBSupport;
20
20
  getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: FirestoreDBOptions): Promise<ROW[]>;
21
21
  runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: FirestoreDBOptions): Promise<RunQueryResult<ROW>>;
22
- runFirestoreQuery<ROW extends ObjectWithId>(q: Query, _opt?: FirestoreDBOptions): Promise<ROW[]>;
22
+ runFirestoreQuery<ROW extends ObjectWithId>(q: Query): Promise<ROW[]>;
23
23
  runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: FirestoreDBOptions): Promise<number>;
24
24
  streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBStreamOptions): ReadableTyped<ROW>;
25
25
  saveBatch<ROW extends ObjectWithId>(table: string, rows: ROW[], opt?: FirestoreDBSaveOptions<ROW>): Promise<void>;
@@ -57,14 +57,14 @@ export class FirestoreDB extends BaseCommonDB {
57
57
  };
58
58
  }
59
59
  const firestoreQuery = dbQueryToFirestoreQuery(q, this.cfg.firestore.collection(q.table));
60
- let rows = await this.runFirestoreQuery(firestoreQuery, opt);
60
+ let rows = await this.runFirestoreQuery(firestoreQuery);
61
61
  // Special case when projection query didn't specify 'id'
62
62
  if (q._selectedFieldNames && !q._selectedFieldNames.includes('id')) {
63
63
  rows = rows.map(r => _omit(r, ['id']));
64
64
  }
65
65
  return { rows };
66
66
  }
67
- async runFirestoreQuery(q, _opt) {
67
+ async runFirestoreQuery(q) {
68
68
  return this.querySnapshotToArray(await q.get());
69
69
  }
70
70
  async runQueryCount(q, _opt) {
@@ -88,21 +88,22 @@ export class FirestoreDB extends BaseCommonDB {
88
88
  const method = methodMap[opt.saveMethod] || 'set';
89
89
  if (opt.tx) {
90
90
  const { tx } = opt.tx;
91
- rows.forEach(row => {
91
+ for (const row of rows) {
92
92
  _assert(row.id, `firestore-db doesn't support id auto-generation, but empty id was provided in saveBatch`);
93
- tx[method](col.doc(escapeDocId(row.id)), _filterUndefinedValues(row));
94
- });
93
+ const { id, ...rowWithoutId } = row;
94
+ tx[method](col.doc(escapeDocId(id)), _filterUndefinedValues(rowWithoutId));
95
+ }
95
96
  return;
96
97
  }
97
- // Firestore allows max 500 items in one batch
98
- await pMap(_chunk(rows, 500), async (chunk) => {
98
+ await pMap(_chunk(rows, MAX_ITEMS), async (chunk) => {
99
99
  const batch = firestore.batch();
100
- chunk.forEach(row => {
100
+ for (const row of chunk) {
101
101
  _assert(row.id, `firestore-db doesn't support id auto-generation, but empty id was provided in saveBatch`);
102
- batch[method](col.doc(escapeDocId(row.id)), _filterUndefinedValues(row));
103
- });
102
+ const { id, ...rowWithoutId } = row;
103
+ batch[method](col.doc(escapeDocId(id)), _filterUndefinedValues(rowWithoutId));
104
+ }
104
105
  await batch.commit();
105
- }, { concurrency: 1 });
106
+ }, { concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY });
106
107
  }
107
108
  // DELETE
108
109
  async deleteByQuery(q, opt) {
@@ -123,29 +124,27 @@ export class FirestoreDB extends BaseCommonDB {
123
124
  const col = firestore.collection(table);
124
125
  if (opt.tx) {
125
126
  const { tx } = opt.tx;
126
- ids.forEach(id => {
127
+ for (const id of ids) {
127
128
  tx.delete(col.doc(escapeDocId(id)));
128
- });
129
+ }
129
130
  return ids.length;
130
131
  }
131
- await pMap(_chunk(ids, 500), async (chunk) => {
132
+ await pMap(_chunk(ids, MAX_ITEMS), async (chunk) => {
132
133
  const batch = firestore.batch();
133
- chunk.forEach(id => {
134
+ for (const id of chunk) {
134
135
  batch.delete(col.doc(escapeDocId(id)));
135
- });
136
+ }
136
137
  await batch.commit();
138
+ }, {
139
+ concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY,
137
140
  });
138
141
  return ids.length;
139
142
  }
140
143
  querySnapshotToArray(qs) {
141
- const rows = [];
142
- qs.forEach(doc => {
143
- rows.push({
144
- id: unescapeDocId(doc.id),
145
- ...doc.data(),
146
- });
147
- });
148
- return rows;
144
+ return qs.docs.map(doc => ({
145
+ id: unescapeDocId(doc.id),
146
+ ...doc.data(),
147
+ }));
149
148
  }
150
149
  async runInTransaction(fn, opt = {}) {
151
150
  const { readOnly } = opt;
@@ -174,7 +173,6 @@ export class FirestoreDB extends BaseCommonDB {
174
173
  const batch = firestore.batch();
175
174
  for (const [id, increment] of _stringMapEntries(incrementMap)) {
176
175
  batch.set(col.doc(escapeDocId(id)), {
177
- // todo: lazy-load FieldValue
178
176
  [prop]: FieldValue.increment(increment),
179
177
  }, { merge: true });
180
178
  }
@@ -214,3 +212,7 @@ export class FirestoreDBTransaction {
214
212
  return await this.db.deleteByIds(table, ids, { ...opt, tx: this });
215
213
  }
216
214
  }
215
+ // Datastore (also Firestore and other Google APIs) supports max 500 of items when saving/deleting, etc.
216
+ const MAX_ITEMS = 500;
217
+ // It's an empyrical value, but anything less than infinity is better than infinity
218
+ const FIRESTORE_RECOMMENDED_CONCURRENCY = 8;
@@ -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
- // eslint-disable-next-line unicorn/no-array-reduce
10
- let q = dbQuery._filters.reduce((q, f) => {
11
- return q.where(f.name, OP_MAP[f.op] || f.op, f.val);
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
- // eslint-disable-next-line unicorn/no-array-reduce
15
- q = dbQuery._orders.reduce((q, ord) => {
16
- return q.orderBy(ord.name, ord.descending ? 'desc' : 'asc');
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
@@ -12,7 +12,7 @@
12
12
  "@types/node": "^24",
13
13
  "dotenv": "^17",
14
14
  "firebase-admin": "^13",
15
- "@naturalcycles/dev-lib": "19.11.0"
15
+ "@naturalcycles/dev-lib": "18.4.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.3.0",
41
+ "version": "2.5.0",
42
42
  "description": "Firestore implementation of CommonDB interface",
43
43
  "author": "Natural Cycles Team",
44
44
  "license": "MIT",
@@ -85,8 +85,8 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
85
85
  if (data === undefined) return
86
86
  return {
87
87
  id: unescapeDocId(doc.id),
88
- ...(data as any),
89
- }
88
+ ...data,
89
+ } as ROW
90
90
  })
91
91
  .filter(_isTruthy)
92
92
  }
@@ -106,7 +106,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
106
106
 
107
107
  const firestoreQuery = dbQueryToFirestoreQuery(q, this.cfg.firestore.collection(q.table))
108
108
 
109
- let rows = await this.runFirestoreQuery<ROW>(firestoreQuery, opt)
109
+ let rows = await this.runFirestoreQuery<ROW>(firestoreQuery)
110
110
 
111
111
  // Special case when projection query didn't specify 'id'
112
112
  if (q._selectedFieldNames && !q._selectedFieldNames.includes('id')) {
@@ -116,10 +116,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
116
116
  return { rows }
117
117
  }
118
118
 
119
- async runFirestoreQuery<ROW extends ObjectWithId>(
120
- q: Query,
121
- _opt?: FirestoreDBOptions,
122
- ): Promise<ROW[]> {
119
+ async runFirestoreQuery<ROW extends ObjectWithId>(q: Query): Promise<ROW[]> {
123
120
  return this.querySnapshotToArray(await q.get())
124
121
  }
125
122
 
@@ -159,37 +156,41 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
159
156
  if (opt.tx) {
160
157
  const { tx } = opt.tx as FirestoreDBTransaction
161
158
 
162
- rows.forEach(row => {
159
+ for (const row of rows) {
163
160
  _assert(
164
161
  row.id,
165
162
  `firestore-db doesn't support id auto-generation, but empty id was provided in saveBatch`,
166
163
  )
167
164
 
168
- tx[method as 'set' | 'create'](col.doc(escapeDocId(row.id)), _filterUndefinedValues(row))
169
- })
165
+ const { id, ...rowWithoutId } = row
166
+ tx[method as 'set' | 'create'](
167
+ col.doc(escapeDocId(id)),
168
+ _filterUndefinedValues(rowWithoutId),
169
+ )
170
+ }
170
171
  return
171
172
  }
172
173
 
173
- // Firestore allows max 500 items in one batch
174
174
  await pMap(
175
- _chunk(rows, 500),
175
+ _chunk(rows, MAX_ITEMS),
176
176
  async chunk => {
177
177
  const batch = firestore.batch()
178
178
 
179
- chunk.forEach(row => {
179
+ for (const row of chunk) {
180
180
  _assert(
181
181
  row.id,
182
182
  `firestore-db doesn't support id auto-generation, but empty id was provided in saveBatch`,
183
183
  )
184
+ const { id, ...rowWithoutId } = row
184
185
  batch[method as 'set' | 'create'](
185
- col.doc(escapeDocId(row.id)),
186
- _filterUndefinedValues(row),
186
+ col.doc(escapeDocId(id)),
187
+ _filterUndefinedValues(rowWithoutId),
187
188
  )
188
- })
189
+ }
189
190
 
190
191
  await batch.commit()
191
192
  },
192
- { concurrency: 1 },
193
+ { concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY },
193
194
  )
194
195
  }
195
196
 
@@ -208,7 +209,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
208
209
  q.select([]),
209
210
  this.cfg.firestore.collection(q.table),
210
211
  )
211
- ids = (await this.runFirestoreQuery<ROW>(firestoreQuery)).map(obj => obj.id)
212
+ ids = (await this.runFirestoreQuery<ObjectWithId>(firestoreQuery)).map(obj => obj.id)
212
213
  }
213
214
 
214
215
  await this.deleteByIds(q.table, ids, opt)
@@ -227,36 +228,39 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
227
228
  if (opt.tx) {
228
229
  const { tx } = opt.tx as FirestoreDBTransaction
229
230
 
230
- ids.forEach(id => {
231
+ for (const id of ids) {
231
232
  tx.delete(col.doc(escapeDocId(id)))
232
- })
233
+ }
233
234
  return ids.length
234
235
  }
235
236
 
236
- await pMap(_chunk(ids, 500), async chunk => {
237
- const batch = firestore.batch()
237
+ await pMap(
238
+ _chunk(ids, MAX_ITEMS),
239
+ async chunk => {
240
+ const batch = firestore.batch()
238
241
 
239
- chunk.forEach(id => {
240
- batch.delete(col.doc(escapeDocId(id)))
241
- })
242
+ for (const id of chunk) {
243
+ batch.delete(col.doc(escapeDocId(id)))
244
+ }
242
245
 
243
- await batch.commit()
244
- })
246
+ await batch.commit()
247
+ },
248
+ {
249
+ concurrency: FIRESTORE_RECOMMENDED_CONCURRENCY,
250
+ },
251
+ )
245
252
 
246
253
  return ids.length
247
254
  }
248
255
 
249
256
  private querySnapshotToArray<T = any>(qs: QuerySnapshot): T[] {
250
- const rows: any[] = []
251
-
252
- qs.forEach(doc => {
253
- rows.push({
254
- id: unescapeDocId(doc.id),
255
- ...doc.data(),
256
- })
257
- })
258
-
259
- return rows
257
+ return qs.docs.map(
258
+ doc =>
259
+ ({
260
+ id: unescapeDocId(doc.id),
261
+ ...doc.data(),
262
+ }) as T,
263
+ )
260
264
  }
261
265
 
262
266
  override async runInTransaction(
@@ -301,7 +305,6 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
301
305
  batch.set(
302
306
  col.doc(escapeDocId(id)),
303
307
  {
304
- // todo: lazy-load FieldValue
305
308
  [prop]: FieldValue.increment(increment),
306
309
  },
307
310
  { merge: true },
@@ -358,3 +361,8 @@ export class FirestoreDBTransaction implements DBTransaction {
358
361
  return await this.db.deleteByIds(table, ids, { ...opt, tx: this })
359
362
  }
360
363
  }
364
+
365
+ // Datastore (also Firestore and other Google APIs) supports max 500 of items when saving/deleting, etc.
366
+ const MAX_ITEMS = 500
367
+ // It's an empyrical value, but anything less than infinity is better than infinity
368
+ const FIRESTORE_RECOMMENDED_CONCURRENCY = 8
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
- // eslint-disable-next-line unicorn/no-array-reduce
18
- let q = dbQuery._filters.reduce((q, f) => {
19
- return q.where(f.name as string, OP_MAP[f.op] || (f.op as WhereFilterOp), f.val)
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
- // eslint-disable-next-line unicorn/no-array-reduce
24
- q = dbQuery._orders.reduce((q, ord) => {
25
- return q.orderBy(ord.name as string, ord.descending ? 'desc' : 'asc')
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)