@naturalcycles/firestore-lib 1.5.1 → 1.6.1

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.
@@ -1,5 +1,5 @@
1
- import { Firestore, Query } from '@google-cloud/firestore';
2
- import { BaseCommonDB, CommonDB, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, DBQuery, DBTransaction, RunQueryResult } from '@naturalcycles/db-lib';
1
+ import { Firestore, Query, Transaction } from '@google-cloud/firestore';
2
+ import { BaseCommonDB, CommonDB, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, CommonDBSupport, DBQuery, DBTransaction, DBTransactionFn, RunQueryResult } from '@naturalcycles/db-lib';
3
3
  import { ObjectWithId, AnyObjectWithId } from '@naturalcycles/js-lib';
4
4
  import { ReadableTyped } from '@naturalcycles/nodejs-lib';
5
5
  export interface FirestoreDBCfg {
@@ -9,19 +9,35 @@ export interface FirestoreDBOptions extends CommonDBOptions {
9
9
  }
10
10
  export interface FirestoreDBSaveOptions<ROW extends Partial<ObjectWithId> = AnyObjectWithId> extends CommonDBSaveOptions<ROW> {
11
11
  }
12
+ export declare class RollbackError extends Error {
13
+ constructor();
14
+ }
12
15
  export declare class FirestoreDB extends BaseCommonDB implements CommonDB {
13
16
  cfg: FirestoreDBCfg;
14
17
  constructor(cfg: FirestoreDBCfg);
15
- getByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][], _opt?: FirestoreDBOptions): Promise<ROW[]>;
18
+ support: CommonDBSupport;
19
+ getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: FirestoreDBOptions): Promise<ROW[]>;
16
20
  runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: FirestoreDBOptions): Promise<RunQueryResult<ROW>>;
17
21
  runFirestoreQuery<ROW extends ObjectWithId>(q: Query, _opt?: FirestoreDBOptions): Promise<ROW[]>;
18
22
  runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: FirestoreDBOptions): Promise<number>;
19
23
  streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBStreamOptions): ReadableTyped<ROW>;
20
24
  saveBatch<ROW extends Partial<ObjectWithId>>(table: string, rows: ROW[], opt?: FirestoreDBSaveOptions<ROW>): Promise<void>;
21
25
  deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: FirestoreDBOptions): Promise<number>;
22
- deleteByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][], _opt?: FirestoreDBOptions): Promise<number>;
26
+ deleteByIds(table: string, ids: string[], opt?: FirestoreDBOptions): Promise<number>;
23
27
  private querySnapshotToArray;
24
- commitTransaction(tx: DBTransaction, _opt?: CommonDBSaveOptions): Promise<void>;
28
+ runInTransaction(fn: DBTransactionFn): Promise<void>;
25
29
  ping(): Promise<void>;
26
30
  getTables(): Promise<string[]>;
27
31
  }
32
+ /**
33
+ * https://firebase.google.com/docs/firestore/manage-data/transactions
34
+ */
35
+ export declare class FirestoreDBTransaction implements DBTransaction {
36
+ db: FirestoreDB;
37
+ tx: Transaction;
38
+ constructor(db: FirestoreDB, tx: Transaction);
39
+ rollback(): Promise<void>;
40
+ getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: CommonDBOptions): Promise<ROW[]>;
41
+ saveBatch<ROW extends Partial<ObjectWithId>>(table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
42
+ deleteByIds(table: string, ids: string[], opt?: CommonDBOptions): Promise<number>;
43
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FirestoreDB = void 0;
3
+ exports.FirestoreDBTransaction = exports.FirestoreDB = exports.RollbackError = void 0;
4
4
  const db_lib_1 = require("@naturalcycles/db-lib");
5
5
  const js_lib_1 = require("@naturalcycles/js-lib");
6
6
  const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
@@ -11,23 +11,29 @@ const methodMap = {
11
11
  update: 'update',
12
12
  upsert: 'set',
13
13
  };
14
+ class RollbackError extends Error {
15
+ constructor() {
16
+ super('rollback');
17
+ }
18
+ }
19
+ exports.RollbackError = RollbackError;
14
20
  class FirestoreDB extends db_lib_1.BaseCommonDB {
15
21
  constructor(cfg) {
16
22
  super();
17
23
  this.cfg = cfg;
24
+ this.support = {
25
+ ...db_lib_1.commonDBFullSupport,
26
+ updateByQuery: false,
27
+ tableSchemas: false,
28
+ };
18
29
  }
19
30
  // GET
20
- async getByIds(table, ids, _opt) {
21
- // Oj, doesn't look like a very optimal implementation!
22
- // TODO: check if we can query by keys or smth
23
- // return (await Promise.all(ids.map(id => this.getById<ROW>(table, id, opt)))).filter(
24
- // Boolean,
25
- // ) as ROW[]
31
+ async getByIds(table, ids, opt = {}) {
26
32
  if (!ids.length)
27
33
  return [];
28
34
  const { firestore } = this.cfg;
29
35
  const col = firestore.collection(table);
30
- return (await firestore.getAll(...ids.map(id => col.doc((0, firestore_util_1.escapeDocId)(id)))))
36
+ return (await (opt.tx?.tx || firestore).getAll(...ids.map(id => col.doc((0, firestore_util_1.escapeDocId)(id)))))
31
37
  .map(doc => {
32
38
  const data = doc.data();
33
39
  if (data === undefined)
@@ -79,13 +85,23 @@ class FirestoreDB extends db_lib_1.BaseCommonDB {
79
85
  }
80
86
  // SAVE
81
87
  async saveBatch(table, rows, opt = {}) {
88
+ const { firestore } = this.cfg;
89
+ const col = firestore.collection(table);
82
90
  const method = methodMap[opt.saveMethod] || 'set';
91
+ if (opt.tx) {
92
+ const { tx } = opt.tx;
93
+ rows.forEach(row => {
94
+ (0, js_lib_1._assert)(row.id, `firestore-db doesn't support id auto-generation, but empty id was provided in saveBatch`);
95
+ tx[method](col.doc((0, firestore_util_1.escapeDocId)(row.id)), (0, js_lib_1._filterUndefinedValues)(row));
96
+ });
97
+ return;
98
+ }
83
99
  // Firestore allows max 500 items in one batch
84
100
  await (0, js_lib_1.pMap)((0, js_lib_1._chunk)(rows, 500), async (chunk) => {
85
- const batch = this.cfg.firestore.batch();
101
+ const batch = firestore.batch();
86
102
  chunk.forEach(row => {
87
103
  (0, js_lib_1._assert)(row.id, `firestore-db doesn't support id auto-generation, but empty id was provided in saveBatch`);
88
- batch[method](this.cfg.firestore.collection(table).doc((0, firestore_util_1.escapeDocId)(row.id)), (0, js_lib_1._filterUndefinedValues)(row));
104
+ batch[method](col.doc((0, firestore_util_1.escapeDocId)(row.id)), (0, js_lib_1._filterUndefinedValues)(row));
89
105
  });
90
106
  await batch.commit();
91
107
  }, { concurrency: 1 });
@@ -104,11 +120,20 @@ class FirestoreDB extends db_lib_1.BaseCommonDB {
104
120
  await this.deleteByIds(q.table, ids, opt);
105
121
  return ids.length;
106
122
  }
107
- async deleteByIds(table, ids, _opt) {
123
+ async deleteByIds(table, ids, opt = {}) {
124
+ const { firestore } = this.cfg;
125
+ const col = firestore.collection(table);
126
+ if (opt.tx) {
127
+ const { tx } = opt.tx;
128
+ ids.forEach(id => {
129
+ tx.delete(col.doc((0, firestore_util_1.escapeDocId)(id)));
130
+ });
131
+ return ids.length;
132
+ }
108
133
  await (0, js_lib_1.pMap)((0, js_lib_1._chunk)(ids, 500), async (chunk) => {
109
- const batch = this.cfg.firestore.batch();
134
+ const batch = firestore.batch();
110
135
  chunk.forEach(id => {
111
- batch.delete(this.cfg.firestore.collection(table).doc((0, firestore_util_1.escapeDocId)(id)));
136
+ batch.delete(col.doc((0, firestore_util_1.escapeDocId)(id)));
112
137
  });
113
138
  await batch.commit();
114
139
  });
@@ -124,25 +149,20 @@ class FirestoreDB extends db_lib_1.BaseCommonDB {
124
149
  });
125
150
  return rows;
126
151
  }
127
- async commitTransaction(tx, _opt) {
128
- const { firestore } = this.cfg;
129
- await firestore.runTransaction(async (tr) => {
130
- for (const op of tx.ops) {
131
- if (op.type === 'saveBatch') {
132
- op.rows.forEach(row => {
133
- tr.set(firestore.collection(op.table).doc((0, firestore_util_1.escapeDocId)(row.id)), (0, js_lib_1._filterUndefinedValues)(row));
134
- });
135
- }
136
- else if (op.type === 'deleteByIds') {
137
- op.ids.forEach(id => {
138
- tr.delete(firestore.collection(op.table).doc((0, firestore_util_1.escapeDocId)(id)));
139
- });
140
- }
141
- else {
142
- throw new Error(`DBOperation not supported: ${op.type}`);
143
- }
152
+ async runInTransaction(fn) {
153
+ try {
154
+ await this.cfg.firestore.runTransaction(async (firestoreTx) => {
155
+ const tx = new FirestoreDBTransaction(this, firestoreTx);
156
+ await fn(tx);
157
+ });
158
+ }
159
+ catch (err) {
160
+ if (err instanceof RollbackError) {
161
+ // RollbackError should be handled gracefully (not re-throw)
162
+ return;
144
163
  }
145
- });
164
+ throw err;
165
+ }
146
166
  }
147
167
  async ping() {
148
168
  // no-op now
@@ -152,3 +172,25 @@ class FirestoreDB extends db_lib_1.BaseCommonDB {
152
172
  }
153
173
  }
154
174
  exports.FirestoreDB = FirestoreDB;
175
+ /**
176
+ * https://firebase.google.com/docs/firestore/manage-data/transactions
177
+ */
178
+ class FirestoreDBTransaction {
179
+ constructor(db, tx) {
180
+ this.db = db;
181
+ this.tx = tx;
182
+ }
183
+ async rollback() {
184
+ throw new RollbackError();
185
+ }
186
+ async getByIds(table, ids, opt) {
187
+ return await this.db.getByIds(table, ids, { ...opt, tx: this });
188
+ }
189
+ async saveBatch(table, rows, opt) {
190
+ await this.db.saveBatch(table, rows, { ...opt, tx: this });
191
+ }
192
+ async deleteByIds(table, ids, opt) {
193
+ return await this.db.deleteByIds(table, ids, { ...opt, tx: this });
194
+ }
195
+ }
196
+ exports.FirestoreDBTransaction = FirestoreDBTransaction;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Firestore } from '@google-cloud/firestore';
2
- import { FirestoreDB, FirestoreDBCfg, FirestoreDBOptions, FirestoreDBSaveOptions } from './firestore.db';
3
- import { dbQueryToFirestoreQuery } from './query.util';
4
- export type { FirestoreDBCfg, FirestoreDBOptions, FirestoreDBSaveOptions };
5
- export { Firestore, FirestoreDB, dbQueryToFirestoreQuery };
2
+ export * from './firestore.db';
3
+ export * from './query.util';
4
+ export { Firestore };
package/dist/index.js CHANGED
@@ -1,9 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.dbQueryToFirestoreQuery = exports.FirestoreDB = exports.Firestore = void 0;
3
+ exports.Firestore = void 0;
4
+ const tslib_1 = require("tslib");
4
5
  const firestore_1 = require("@google-cloud/firestore");
5
6
  Object.defineProperty(exports, "Firestore", { enumerable: true, get: function () { return firestore_1.Firestore; } });
6
- const firestore_db_1 = require("./firestore.db");
7
- Object.defineProperty(exports, "FirestoreDB", { enumerable: true, get: function () { return firestore_db_1.FirestoreDB; } });
8
- const query_util_1 = require("./query.util");
9
- Object.defineProperty(exports, "dbQueryToFirestoreQuery", { enumerable: true, get: function () { return query_util_1.dbQueryToFirestoreQuery; } });
7
+ tslib_1.__exportStar(require("./firestore.db"), exports);
8
+ tslib_1.__exportStar(require("./query.util"), exports);
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "dependencies": {
7
7
  "@google-cloud/firestore": "^7.0.0",
8
- "@naturalcycles/db-lib": "^8.2.0",
8
+ "@naturalcycles/db-lib": "^9.0.0",
9
9
  "@naturalcycles/js-lib": "^14.6.0",
10
10
  "@naturalcycles/nodejs-lib": "^13.1.0"
11
11
  },
@@ -37,7 +37,7 @@
37
37
  "engines": {
38
38
  "node": ">=18.12.0"
39
39
  },
40
- "version": "1.5.1",
40
+ "version": "1.6.1",
41
41
  "description": "Firestore implementation of CommonDB interface",
42
42
  "author": "Natural Cycles Team",
43
43
  "license": "MIT"
@@ -1,13 +1,22 @@
1
- import { Firestore, Query, QueryDocumentSnapshot, QuerySnapshot } from '@google-cloud/firestore'
1
+ import {
2
+ Firestore,
3
+ Query,
4
+ QueryDocumentSnapshot,
5
+ QuerySnapshot,
6
+ Transaction,
7
+ } from '@google-cloud/firestore'
2
8
  import {
3
9
  BaseCommonDB,
4
10
  CommonDB,
11
+ commonDBFullSupport,
5
12
  CommonDBOptions,
6
13
  CommonDBSaveMethod,
7
14
  CommonDBSaveOptions,
8
15
  CommonDBStreamOptions,
16
+ CommonDBSupport,
9
17
  DBQuery,
10
18
  DBTransaction,
19
+ DBTransactionFn,
11
20
  RunQueryResult,
12
21
  } from '@naturalcycles/db-lib'
13
22
  import {
@@ -33,34 +42,47 @@ export interface FirestoreDBOptions extends CommonDBOptions {}
33
42
  export interface FirestoreDBSaveOptions<ROW extends Partial<ObjectWithId> = AnyObjectWithId>
34
43
  extends CommonDBSaveOptions<ROW> {}
35
44
 
36
- const methodMap: Record<CommonDBSaveMethod, string> = {
45
+ type SaveOp = 'create' | 'update' | 'set'
46
+
47
+ const methodMap: Record<CommonDBSaveMethod, SaveOp> = {
37
48
  insert: 'create',
38
49
  update: 'update',
39
50
  upsert: 'set',
40
51
  }
41
52
 
53
+ export class RollbackError extends Error {
54
+ constructor() {
55
+ super('rollback')
56
+ }
57
+ }
58
+
42
59
  export class FirestoreDB extends BaseCommonDB implements CommonDB {
43
60
  constructor(public cfg: FirestoreDBCfg) {
44
61
  super()
45
62
  }
46
63
 
64
+ override support: CommonDBSupport = {
65
+ ...commonDBFullSupport,
66
+ updateByQuery: false,
67
+ tableSchemas: false,
68
+ }
69
+
47
70
  // GET
48
71
  override async getByIds<ROW extends ObjectWithId>(
49
72
  table: string,
50
- ids: ROW['id'][],
51
- _opt?: FirestoreDBOptions,
73
+ ids: string[],
74
+ opt: FirestoreDBOptions = {},
52
75
  ): Promise<ROW[]> {
53
- // Oj, doesn't look like a very optimal implementation!
54
- // TODO: check if we can query by keys or smth
55
- // return (await Promise.all(ids.map(id => this.getById<ROW>(table, id, opt)))).filter(
56
- // Boolean,
57
- // ) as ROW[]
58
76
  if (!ids.length) return []
59
77
 
60
78
  const { firestore } = this.cfg
61
79
  const col = firestore.collection(table)
62
80
 
63
- return (await firestore.getAll(...ids.map(id => col.doc(escapeDocId(id)))))
81
+ return (
82
+ await ((opt.tx as FirestoreDBTransaction)?.tx || firestore).getAll(
83
+ ...ids.map(id => col.doc(escapeDocId(id))),
84
+ )
85
+ )
64
86
  .map(doc => {
65
87
  const data = doc.data()
66
88
  if (data === undefined) return
@@ -143,21 +165,37 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
143
165
  rows: ROW[],
144
166
  opt: FirestoreDBSaveOptions<ROW> = {},
145
167
  ): Promise<void> {
146
- const method = methodMap[opt.saveMethod!] || 'set'
168
+ const { firestore } = this.cfg
169
+ const col = firestore.collection(table)
170
+ const method: SaveOp = methodMap[opt.saveMethod!] || 'set'
171
+
172
+ if (opt.tx) {
173
+ const { tx } = opt.tx as FirestoreDBTransaction
174
+
175
+ rows.forEach(row => {
176
+ _assert(
177
+ row.id,
178
+ `firestore-db doesn't support id auto-generation, but empty id was provided in saveBatch`,
179
+ )
180
+
181
+ tx[method as 'set' | 'create'](col.doc(escapeDocId(row.id)), _filterUndefinedValues(row))
182
+ })
183
+ return
184
+ }
147
185
 
148
186
  // Firestore allows max 500 items in one batch
149
187
  await pMap(
150
188
  _chunk(rows, 500),
151
189
  async chunk => {
152
- const batch = this.cfg.firestore.batch()
190
+ const batch = firestore.batch()
153
191
 
154
192
  chunk.forEach(row => {
155
193
  _assert(
156
194
  row.id,
157
195
  `firestore-db doesn't support id auto-generation, but empty id was provided in saveBatch`,
158
196
  )
159
- ;(batch as any)[method](
160
- this.cfg.firestore.collection(table).doc(escapeDocId(row.id)),
197
+ batch[method as 'set' | 'create'](
198
+ col.doc(escapeDocId(row.id)),
161
199
  _filterUndefinedValues(row),
162
200
  )
163
201
  })
@@ -173,7 +211,7 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
173
211
  q: DBQuery<ROW>,
174
212
  opt?: FirestoreDBOptions,
175
213
  ): Promise<number> {
176
- let ids: ROW['id'][]
214
+ let ids: string[]
177
215
 
178
216
  const idFilter = q._filters.find(f => f.name === 'id')
179
217
  if (idFilter) {
@@ -191,16 +229,28 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
191
229
  return ids.length
192
230
  }
193
231
 
194
- async deleteByIds<ROW extends ObjectWithId>(
232
+ override async deleteByIds(
195
233
  table: string,
196
- ids: ROW['id'][],
197
- _opt?: FirestoreDBOptions,
234
+ ids: string[],
235
+ opt: FirestoreDBOptions = {},
198
236
  ): Promise<number> {
237
+ const { firestore } = this.cfg
238
+ const col = firestore.collection(table)
239
+
240
+ if (opt.tx) {
241
+ const { tx } = opt.tx as FirestoreDBTransaction
242
+
243
+ ids.forEach(id => {
244
+ tx.delete(col.doc(escapeDocId(id)))
245
+ })
246
+ return ids.length
247
+ }
248
+
199
249
  await pMap(_chunk(ids, 500), async chunk => {
200
- const batch = this.cfg.firestore.batch()
250
+ const batch = firestore.batch()
201
251
 
202
252
  chunk.forEach(id => {
203
- batch.delete(this.cfg.firestore.collection(table).doc(escapeDocId(id)))
253
+ batch.delete(col.doc(escapeDocId(id)))
204
254
  })
205
255
 
206
256
  await batch.commit()
@@ -222,27 +272,19 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
222
272
  return rows
223
273
  }
224
274
 
225
- override async commitTransaction(tx: DBTransaction, _opt?: CommonDBSaveOptions): Promise<void> {
226
- const { firestore } = this.cfg
227
-
228
- await firestore.runTransaction(async tr => {
229
- for (const op of tx.ops) {
230
- if (op.type === 'saveBatch') {
231
- op.rows.forEach(row => {
232
- tr.set(
233
- firestore.collection(op.table).doc(escapeDocId(row.id)),
234
- _filterUndefinedValues(row),
235
- )
236
- })
237
- } else if (op.type === 'deleteByIds') {
238
- op.ids.forEach(id => {
239
- tr.delete(firestore.collection(op.table).doc(escapeDocId(id)))
240
- })
241
- } else {
242
- throw new Error(`DBOperation not supported: ${(op as any).type}`)
243
- }
275
+ override async runInTransaction(fn: DBTransactionFn): Promise<void> {
276
+ try {
277
+ await this.cfg.firestore.runTransaction(async firestoreTx => {
278
+ const tx = new FirestoreDBTransaction(this, firestoreTx)
279
+ await fn(tx)
280
+ })
281
+ } catch (err) {
282
+ if (err instanceof RollbackError) {
283
+ // RollbackError should be handled gracefully (not re-throw)
284
+ return
244
285
  }
245
- })
286
+ throw err
287
+ }
246
288
  }
247
289
 
248
290
  override async ping(): Promise<void> {
@@ -253,3 +295,37 @@ export class FirestoreDB extends BaseCommonDB implements CommonDB {
253
295
  return []
254
296
  }
255
297
  }
298
+
299
+ /**
300
+ * https://firebase.google.com/docs/firestore/manage-data/transactions
301
+ */
302
+ export class FirestoreDBTransaction implements DBTransaction {
303
+ constructor(
304
+ public db: FirestoreDB,
305
+ public tx: Transaction,
306
+ ) {}
307
+
308
+ async rollback(): Promise<void> {
309
+ throw new RollbackError()
310
+ }
311
+
312
+ async getByIds<ROW extends ObjectWithId>(
313
+ table: string,
314
+ ids: string[],
315
+ opt?: CommonDBOptions,
316
+ ): Promise<ROW[]> {
317
+ return await this.db.getByIds(table, ids, { ...opt, tx: this })
318
+ }
319
+
320
+ async saveBatch<ROW extends Partial<ObjectWithId>>(
321
+ table: string,
322
+ rows: ROW[],
323
+ opt?: CommonDBSaveOptions<ROW>,
324
+ ): Promise<void> {
325
+ await this.db.saveBatch(table, rows, { ...opt, tx: this })
326
+ }
327
+
328
+ async deleteByIds(table: string, ids: string[], opt?: CommonDBOptions): Promise<number> {
329
+ return await this.db.deleteByIds(table, ids, { ...opt, tx: this })
330
+ }
331
+ }
package/src/index.ts CHANGED
@@ -1,12 +1,4 @@
1
1
  import { Firestore } from '@google-cloud/firestore'
2
- import {
3
- FirestoreDB,
4
- FirestoreDBCfg,
5
- FirestoreDBOptions,
6
- FirestoreDBSaveOptions,
7
- } from './firestore.db'
8
- import { dbQueryToFirestoreQuery } from './query.util'
9
-
10
- export type { FirestoreDBCfg, FirestoreDBOptions, FirestoreDBSaveOptions }
11
-
12
- export { Firestore, FirestoreDB, dbQueryToFirestoreQuery }
2
+ export * from './firestore.db'
3
+ export * from './query.util'
4
+ export { Firestore }