@naturalcycles/firestore-lib 1.1.4

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/CHANGELOG.md ADDED
@@ -0,0 +1,105 @@
1
+ ## [1.1.4](https://github.com/NaturalCycles/firestore-lib/compare/v1.1.3...v1.1.4) (2021-10-01)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * adapt to db-lib ([05cbc49](https://github.com/NaturalCycles/firestore-lib/commit/05cbc4996efe5a3f1091835b016e2534b59befde))
7
+
8
+ ## [1.1.3](https://github.com/NaturalCycles/firestore-lib/compare/v1.1.2...v1.1.3) (2021-08-25)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * deps ([541438a](https://github.com/NaturalCycles/firestore-lib/commit/541438aab99613d5b0c69b48b68008d6e0fbe06f))
14
+
15
+ ## [1.1.2](https://github.com/NaturalCycles/firestore-lib/compare/v1.1.1...v1.1.2) (2021-02-27)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * deps ([741c1cb](https://github.com/NaturalCycles/firestore-lib/commit/741c1cbd2fbc913a32bdd26d394a14ea9e548f76))
21
+
22
+ ## [1.1.1](https://github.com/NaturalCycles/firestore-lib/compare/v1.1.0...v1.1.1) (2020-11-05)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * adapt to db-lib ([032f821](https://github.com/NaturalCycles/firestore-lib/commit/032f82164f200d75fb5ed6c72ba12209fce45e9c))
28
+
29
+ # [1.1.0](https://github.com/NaturalCycles/firestore-lib/compare/v1.0.9...v1.1.0) (2020-10-25)
30
+
31
+
32
+ ### Features
33
+
34
+ * adapt to current db-lib ([4c2f228](https://github.com/NaturalCycles/firestore-lib/commit/4c2f228c4f80711735a53b20fc75a9a74878ffb9))
35
+
36
+ ## [1.0.9](https://github.com/NaturalCycles/firestore-lib/compare/v1.0.8...v1.0.9) (2020-03-22)
37
+
38
+
39
+ ### Bug Fixes
40
+
41
+ * deps ([53f70bb](https://github.com/NaturalCycles/firestore-lib/commit/53f70bbe9b746c500bb394d506bc0a707f09b4f2))
42
+
43
+ ## [1.0.8](https://github.com/NaturalCycles/firestore-lib/compare/v1.0.7...v1.0.8) (2019-09-21)
44
+
45
+
46
+ ### Bug Fixes
47
+
48
+ * adapt to db-lib ([d046dd6](https://github.com/NaturalCycles/firestore-lib/commit/d046dd6))
49
+
50
+ ## [1.0.7](https://github.com/NaturalCycles/firestore-lib/compare/v1.0.6...v1.0.7) (2019-09-20)
51
+
52
+
53
+ ### Bug Fixes
54
+
55
+ * adapt to new db-lib ([414b97c](https://github.com/NaturalCycles/firestore-lib/commit/414b97c))
56
+
57
+ ## [1.0.6](https://github.com/NaturalCycles/firestore-lib/compare/v1.0.5...v1.0.6) (2019-08-24)
58
+
59
+
60
+ ### Bug Fixes
61
+
62
+ * missing index.ts ([b2b75cd](https://github.com/NaturalCycles/firestore-lib/commit/b2b75cd))
63
+
64
+ ## [1.0.5](https://github.com/NaturalCycles/firestore-lib/compare/v1.0.4...v1.0.5) (2019-08-23)
65
+
66
+
67
+ ### Bug Fixes
68
+
69
+ * adopt to db-lib ([60800db](https://github.com/NaturalCycles/firestore-lib/commit/60800db))
70
+
71
+ ## [1.0.4](https://github.com/NaturalCycles/firestore-lib/compare/v1.0.3...v1.0.4) (2019-08-20)
72
+
73
+
74
+ ### Bug Fixes
75
+
76
+ * return await ([6c6267d](https://github.com/NaturalCycles/firestore-lib/commit/6c6267d))
77
+
78
+ ## [1.0.3](https://github.com/NaturalCycles/firestore-lib/compare/v1.0.2...v1.0.3) (2019-08-20)
79
+
80
+
81
+ ### Bug Fixes
82
+
83
+ * update to recent commondb interface ([d506916](https://github.com/NaturalCycles/firestore-lib/commit/d506916))
84
+
85
+ ## [1.0.2](https://github.com/NaturalCycles/firestore-lib/compare/v1.0.1...v1.0.2) (2019-08-18)
86
+
87
+
88
+ ### Bug Fixes
89
+
90
+ * filterUndefinedValues ([57e0b24](https://github.com/NaturalCycles/firestore-lib/commit/57e0b24))
91
+
92
+ ## [1.0.1](https://github.com/NaturalCycles/firestore-lib/compare/v1.0.0...v1.0.1) (2019-08-18)
93
+
94
+
95
+ ### Bug Fixes
96
+
97
+ * update to recent db-lib ([dbbb093](https://github.com/NaturalCycles/firestore-lib/commit/dbbb093))
98
+
99
+ # 1.0.0 (2019-07-28)
100
+
101
+
102
+ ### Features
103
+
104
+ * first version ([6d129a9](https://github.com/NaturalCycles/firestore-lib/commit/6d129a9))
105
+ * init project by create-module ([205703f](https://github.com/NaturalCycles/firestore-lib/commit/205703f))
@@ -0,0 +1,27 @@
1
+ import { Query } from '@google-cloud/firestore';
2
+ import { BaseCommonDB, CommonDB, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, DBQuery, DBTransaction, ObjectWithId, RunQueryResult } from '@naturalcycles/db-lib';
3
+ import { ReadableTyped } from '@naturalcycles/nodejs-lib';
4
+ import * as firebaseAdmin from 'firebase-admin';
5
+ export interface FirestoreDBCfg {
6
+ firestore: firebaseAdmin.firestore.Firestore;
7
+ }
8
+ export interface FirestoreDBOptions extends CommonDBOptions {
9
+ }
10
+ export interface FirestoreDBSaveOptions extends CommonDBSaveOptions {
11
+ }
12
+ export declare class FirestoreDB extends BaseCommonDB implements CommonDB {
13
+ cfg: FirestoreDBCfg;
14
+ constructor(cfg: FirestoreDBCfg);
15
+ getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: FirestoreDBOptions): Promise<ROW[]>;
16
+ getById<ROW extends ObjectWithId>(table: string, id: string, _opt?: FirestoreDBOptions): Promise<ROW | undefined>;
17
+ runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: FirestoreDBOptions): Promise<RunQueryResult<ROW>>;
18
+ runFirestoreQuery<ROW extends ObjectWithId>(q: Query, _opt?: FirestoreDBOptions): Promise<ROW[]>;
19
+ runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: FirestoreDBOptions): Promise<number>;
20
+ streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBStreamOptions): ReadableTyped<ROW>;
21
+ saveBatch<ROW extends ObjectWithId>(table: string, rows: ROW[], _opt?: FirestoreDBSaveOptions): Promise<void>;
22
+ deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: FirestoreDBOptions): Promise<number>;
23
+ deleteByIds(table: string, ids: string[], _opt?: FirestoreDBOptions): Promise<number>;
24
+ private querySnapshotToArray;
25
+ commitTransaction(_tx: DBTransaction, _opt?: CommonDBSaveOptions): Promise<void>;
26
+ ping(): Promise<void>;
27
+ }
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FirestoreDB = void 0;
4
+ const db_lib_1 = require("@naturalcycles/db-lib");
5
+ const js_lib_1 = require("@naturalcycles/js-lib");
6
+ const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
7
+ const firestore_util_1 = require("./firestore.util");
8
+ const query_util_1 = require("./query.util");
9
+ class FirestoreDB extends db_lib_1.BaseCommonDB {
10
+ constructor(cfg) {
11
+ super();
12
+ this.cfg = cfg;
13
+ }
14
+ // GET
15
+ async getByIds(table, ids, opt) {
16
+ // Oj, doesn't look like a very optimal implementation!
17
+ // TODO: check if we can query by keys or smth
18
+ return (await Promise.all(ids.map(id => this.getById(table, id, opt)))).filter(Boolean);
19
+ }
20
+ async getById(table, id, _opt) {
21
+ const doc = await this.cfg.firestore.collection(table).doc((0, firestore_util_1.escapeDocId)(id)).get();
22
+ const data = doc.data();
23
+ if (data === undefined)
24
+ return;
25
+ return {
26
+ id,
27
+ ...data,
28
+ };
29
+ }
30
+ // QUERY
31
+ async runQuery(q, opt) {
32
+ const firestoreQuery = (0, query_util_1.dbQueryToFirestoreQuery)(q, this.cfg.firestore.collection(q.table));
33
+ let rows = await this.runFirestoreQuery(firestoreQuery, opt);
34
+ // Special case when projection query didn't specify 'id'
35
+ if (q._selectedFieldNames && !q._selectedFieldNames.includes('id')) {
36
+ rows = rows.map(r => (0, js_lib_1._omit)(r, ['id']));
37
+ }
38
+ return { rows };
39
+ }
40
+ async runFirestoreQuery(q, _opt) {
41
+ return this.querySnapshotToArray(await q.get());
42
+ }
43
+ async runQueryCount(q, opt) {
44
+ const { rows } = await this.runQuery(q.select([]), opt);
45
+ return rows.length;
46
+ }
47
+ streamQuery(q, _opt) {
48
+ const firestoreQuery = (0, query_util_1.dbQueryToFirestoreQuery)(q, this.cfg.firestore.collection(q.table));
49
+ return firestoreQuery.stream().pipe((0, nodejs_lib_1.transformMapSimple)(doc => ({
50
+ id: (0, firestore_util_1.unescapeDocId)(doc.id),
51
+ ...doc.data(),
52
+ }), {
53
+ errorMode: js_lib_1.ErrorMode.SUPPRESS, // because .pipe cannot propagate errors
54
+ }));
55
+ }
56
+ // SAVE
57
+ async saveBatch(table, rows, _opt) {
58
+ // Firestore allows max 500 items in one batch
59
+ await (0, js_lib_1.pMap)((0, js_lib_1._chunk)(rows, 500), async (chunk) => {
60
+ const batch = this.cfg.firestore.batch();
61
+ chunk.forEach(row => {
62
+ batch.set(this.cfg.firestore.collection(table).doc((0, firestore_util_1.escapeDocId)(row.id)), (0, js_lib_1._filterNullishValues)(row));
63
+ });
64
+ await batch.commit();
65
+ }, { concurrency: 1 });
66
+ }
67
+ // DELETE
68
+ async deleteByQuery(q, opt) {
69
+ const firestoreQuery = (0, query_util_1.dbQueryToFirestoreQuery)(q.select([]), this.cfg.firestore.collection(q.table));
70
+ const ids = (await this.runFirestoreQuery(firestoreQuery)).map(obj => obj.id);
71
+ await this.deleteByIds(q.table, ids, opt);
72
+ return ids.length;
73
+ }
74
+ async deleteByIds(table, ids, _opt) {
75
+ await (0, js_lib_1.pMap)((0, js_lib_1._chunk)(ids, 500), async (chunk) => {
76
+ const batch = this.cfg.firestore.batch();
77
+ chunk.forEach(id => {
78
+ batch.delete(this.cfg.firestore.collection(table).doc((0, firestore_util_1.escapeDocId)(id)));
79
+ });
80
+ await batch.commit();
81
+ });
82
+ return ids.length;
83
+ }
84
+ querySnapshotToArray(qs) {
85
+ const rows = [];
86
+ qs.forEach(doc => {
87
+ rows.push({
88
+ id: (0, firestore_util_1.unescapeDocId)(doc.id),
89
+ ...doc.data(),
90
+ });
91
+ });
92
+ return rows;
93
+ }
94
+ async commitTransaction(_tx, _opt) {
95
+ throw new Error('commitTransaction is not supported yet');
96
+ }
97
+ async ping() {
98
+ // no-op now
99
+ }
100
+ }
101
+ exports.FirestoreDB = FirestoreDB;
@@ -0,0 +1,2 @@
1
+ export declare function escapeDocId(docId: string): string;
2
+ export declare function unescapeDocId(docId: string): string;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.unescapeDocId = exports.escapeDocId = void 0;
4
+ const string_util_1 = require("./string.util");
5
+ const SLASH = '_SLASH_';
6
+ function escapeDocId(docId) {
7
+ return (0, string_util_1.replaceAll)(docId, '/', SLASH);
8
+ }
9
+ exports.escapeDocId = escapeDocId;
10
+ function unescapeDocId(docId) {
11
+ return (0, string_util_1.replaceAll)(docId, SLASH, '/');
12
+ }
13
+ exports.unescapeDocId = unescapeDocId;
@@ -0,0 +1,4 @@
1
+ import { FirestoreDB, FirestoreDBCfg, FirestoreDBOptions, FirestoreDBSaveOptions } from './firestore.db';
2
+ import { dbQueryToFirestoreQuery } from './query.util';
3
+ export type { FirestoreDBCfg, FirestoreDBOptions, FirestoreDBSaveOptions };
4
+ export { FirestoreDB, dbQueryToFirestoreQuery };
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dbQueryToFirestoreQuery = exports.FirestoreDB = void 0;
4
+ const firestore_db_1 = require("./firestore.db");
5
+ Object.defineProperty(exports, "FirestoreDB", { enumerable: true, get: function () { return firestore_db_1.FirestoreDB; } });
6
+ const query_util_1 = require("./query.util");
7
+ Object.defineProperty(exports, "dbQueryToFirestoreQuery", { enumerable: true, get: function () { return query_util_1.dbQueryToFirestoreQuery; } });
@@ -0,0 +1,3 @@
1
+ import { Query } from '@google-cloud/firestore';
2
+ import { DBQuery, ObjectWithId } from '@naturalcycles/db-lib';
3
+ export declare function dbQueryToFirestoreQuery<ROW extends ObjectWithId>(dbQuery: DBQuery<ROW>, emptyQuery: Query): Query;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dbQueryToFirestoreQuery = void 0;
4
+ // Map DBQueryFilterOp to WhereFilterOp
5
+ // Currently it's fully aligned!
6
+ const OP_MAP = {
7
+ // '=': '==',
8
+ // in: 'array-contains',
9
+ };
10
+ function dbQueryToFirestoreQuery(dbQuery, emptyQuery) {
11
+ // filter
12
+ // eslint-disable-next-line unicorn/no-array-reduce, unicorn/prefer-object-from-entries
13
+ let q = dbQuery._filters.reduce((q, f) => {
14
+ return q.where(f.name, OP_MAP[f.op] || f.op, f.val);
15
+ }, emptyQuery);
16
+ // order
17
+ // eslint-disable-next-line unicorn/no-array-reduce, unicorn/prefer-object-from-entries
18
+ q = dbQuery._orders.reduce((q, ord) => {
19
+ return q.orderBy(ord.name, ord.descending ? 'desc' : 'asc');
20
+ }, q);
21
+ // limit
22
+ q = q.limit(dbQuery._limitValue);
23
+ // selectedFields
24
+ if (dbQuery._selectedFieldNames) {
25
+ // todo: check if at least id / __key__ is required to be set
26
+ q = q.select(...dbQuery._selectedFieldNames);
27
+ }
28
+ return q;
29
+ }
30
+ exports.dbQueryToFirestoreQuery = dbQueryToFirestoreQuery;
@@ -0,0 +1 @@
1
+ export declare function replaceAll(str: string, find: string, replace: string): string;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.replaceAll = void 0;
4
+ // todo: move to... js-lib? lodash-lib?
5
+ function replaceAll(str, find, replace) {
6
+ return str.replace(new RegExp(find, 'g'), replace);
7
+ }
8
+ exports.replaceAll = replaceAll;
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@naturalcycles/firestore-lib",
3
+ "scripts": {
4
+ "prepare": "husky install"
5
+ },
6
+ "dependencies": {
7
+ "@naturalcycles/db-lib": "^8.2.0",
8
+ "@naturalcycles/js-lib": "^14.6.0",
9
+ "@naturalcycles/nodejs-lib": "^12.4.0"
10
+ },
11
+ "peerDependencies": {
12
+ "firebase-admin": ">=9.3.0"
13
+ },
14
+ "devDependencies": {
15
+ "@naturalcycles/dev-lib": "^12.1.2",
16
+ "@naturalcycles/test-lib": "^1.0.3",
17
+ "@types/node": "^16.7.1",
18
+ "dotenv": "^10.0.0",
19
+ "firebase-admin": "^9.3.0",
20
+ "jest": "^27.0.6"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "!src/test",
26
+ "!src/**/*.test.ts",
27
+ "!src/**/__snapshots__",
28
+ "!src/**/__exclude"
29
+ ],
30
+ "main": "dist/index.js",
31
+ "types": "dist/index.d.ts",
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/NaturalCycles/firestore-lib"
38
+ },
39
+ "engines": {
40
+ "node": ">=12.13"
41
+ },
42
+ "version": "1.1.4",
43
+ "description": "Firestore implementation of CommonDB interface",
44
+ "author": "Natural Cycles Team",
45
+ "license": "MIT"
46
+ }
package/readme.md ADDED
@@ -0,0 +1,17 @@
1
+ ## @naturalcycles/firestore-lib
2
+
3
+ > Firestore implementation of CommonDB interface
4
+
5
+ [![npm](https://img.shields.io/npm/v/@naturalcycles/firestore-lib/latest.svg)](https://www.npmjs.com/package/@naturalcycles/firestore-lib)
6
+ [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)
7
+
8
+ # Features
9
+
10
+ - ...
11
+
12
+ # Packaging
13
+
14
+ - `engines.node >= 10.13`: Latest Node.js LTS
15
+ - `main: dist/index.js`: commonjs, es2018
16
+ - `types: dist/index.d.ts`: typescript types
17
+ - `/src` folder with source `*.ts` files included
@@ -0,0 +1,190 @@
1
+ import { Query, QueryDocumentSnapshot, QuerySnapshot } from '@google-cloud/firestore'
2
+ import {
3
+ BaseCommonDB,
4
+ CommonDB,
5
+ CommonDBOptions,
6
+ CommonDBSaveOptions,
7
+ CommonDBStreamOptions,
8
+ DBQuery,
9
+ DBTransaction,
10
+ ObjectWithId,
11
+ RunQueryResult,
12
+ } from '@naturalcycles/db-lib'
13
+ import { ErrorMode, pMap, _chunk, _filterNullishValues, _omit } from '@naturalcycles/js-lib'
14
+ import { ReadableTyped, transformMapSimple } from '@naturalcycles/nodejs-lib'
15
+ import * as firebaseAdmin from 'firebase-admin'
16
+ import { escapeDocId, unescapeDocId } from './firestore.util'
17
+ import { dbQueryToFirestoreQuery } from './query.util'
18
+
19
+ export interface FirestoreDBCfg {
20
+ firestore: firebaseAdmin.firestore.Firestore
21
+ }
22
+
23
+ export interface FirestoreDBOptions extends CommonDBOptions {}
24
+ export interface FirestoreDBSaveOptions extends CommonDBSaveOptions {}
25
+
26
+ export class FirestoreDB extends BaseCommonDB implements CommonDB {
27
+ constructor(public cfg: FirestoreDBCfg) {
28
+ super()
29
+ }
30
+
31
+ // GET
32
+ override async getByIds<ROW extends ObjectWithId>(
33
+ table: string,
34
+ ids: string[],
35
+ opt?: FirestoreDBOptions,
36
+ ): Promise<ROW[]> {
37
+ // Oj, doesn't look like a very optimal implementation!
38
+ // TODO: check if we can query by keys or smth
39
+ return (await Promise.all(ids.map(id => this.getById<ROW>(table, id, opt)))).filter(
40
+ Boolean,
41
+ ) as ROW[]
42
+ }
43
+
44
+ async getById<ROW extends ObjectWithId>(
45
+ table: string,
46
+ id: string,
47
+ _opt?: FirestoreDBOptions,
48
+ ): Promise<ROW | undefined> {
49
+ const doc = await this.cfg.firestore.collection(table).doc(escapeDocId(id)).get()
50
+
51
+ const data = doc.data()
52
+ if (data === undefined) return
53
+
54
+ return {
55
+ id,
56
+ ...(data as any),
57
+ }
58
+ }
59
+
60
+ // QUERY
61
+ override async runQuery<ROW extends ObjectWithId>(
62
+ q: DBQuery<ROW>,
63
+ opt?: FirestoreDBOptions,
64
+ ): Promise<RunQueryResult<ROW>> {
65
+ const firestoreQuery = dbQueryToFirestoreQuery(q, this.cfg.firestore.collection(q.table))
66
+
67
+ let rows = await this.runFirestoreQuery<ROW>(firestoreQuery, opt)
68
+
69
+ // Special case when projection query didn't specify 'id'
70
+ if (q._selectedFieldNames && !q._selectedFieldNames.includes('id')) {
71
+ rows = rows.map(r => _omit(r as any, ['id']))
72
+ }
73
+
74
+ return { rows }
75
+ }
76
+
77
+ async runFirestoreQuery<ROW extends ObjectWithId>(
78
+ q: Query,
79
+ _opt?: FirestoreDBOptions,
80
+ ): Promise<ROW[]> {
81
+ return this.querySnapshotToArray(await q.get())
82
+ }
83
+
84
+ override async runQueryCount<ROW extends ObjectWithId>(
85
+ q: DBQuery<ROW>,
86
+ opt?: FirestoreDBOptions,
87
+ ): Promise<number> {
88
+ const { rows } = await this.runQuery(q.select([]), opt)
89
+ return rows.length
90
+ }
91
+
92
+ override streamQuery<ROW extends ObjectWithId>(
93
+ q: DBQuery<ROW>,
94
+ _opt?: CommonDBStreamOptions,
95
+ ): ReadableTyped<ROW> {
96
+ const firestoreQuery = dbQueryToFirestoreQuery(q, this.cfg.firestore.collection(q.table))
97
+
98
+ return firestoreQuery.stream().pipe(
99
+ transformMapSimple<QueryDocumentSnapshot<any>, ROW>(
100
+ doc => ({
101
+ id: unescapeDocId(doc.id),
102
+ ...doc.data(),
103
+ }),
104
+ {
105
+ errorMode: ErrorMode.SUPPRESS, // because .pipe cannot propagate errors
106
+ },
107
+ ),
108
+ )
109
+ }
110
+
111
+ // SAVE
112
+ override async saveBatch<ROW extends ObjectWithId>(
113
+ table: string,
114
+ rows: ROW[],
115
+ _opt?: FirestoreDBSaveOptions,
116
+ ): Promise<void> {
117
+ // Firestore allows max 500 items in one batch
118
+ await pMap(
119
+ _chunk(rows, 500),
120
+ async chunk => {
121
+ const batch = this.cfg.firestore.batch()
122
+
123
+ chunk.forEach(row => {
124
+ batch.set(
125
+ this.cfg.firestore.collection(table).doc(escapeDocId(row.id)),
126
+ _filterNullishValues(row), // todo: check if we really need to filter them (thinking of null values)
127
+ )
128
+ })
129
+
130
+ await batch.commit()
131
+ },
132
+ { concurrency: 1 },
133
+ )
134
+ }
135
+
136
+ // DELETE
137
+ override async deleteByQuery<ROW extends ObjectWithId>(
138
+ q: DBQuery<ROW>,
139
+ opt?: FirestoreDBOptions,
140
+ ): Promise<number> {
141
+ const firestoreQuery = dbQueryToFirestoreQuery(
142
+ q.select([]),
143
+ this.cfg.firestore.collection(q.table),
144
+ )
145
+ const ids = (await this.runFirestoreQuery<ROW>(firestoreQuery)).map(obj => obj.id)
146
+
147
+ await this.deleteByIds(q.table, ids, opt)
148
+
149
+ return ids.length
150
+ }
151
+
152
+ override async deleteByIds(
153
+ table: string,
154
+ ids: string[],
155
+ _opt?: FirestoreDBOptions,
156
+ ): Promise<number> {
157
+ await pMap(_chunk(ids, 500), async chunk => {
158
+ const batch = this.cfg.firestore.batch()
159
+
160
+ chunk.forEach(id => {
161
+ batch.delete(this.cfg.firestore.collection(table).doc(escapeDocId(id)))
162
+ })
163
+
164
+ await batch.commit()
165
+ })
166
+
167
+ return ids.length
168
+ }
169
+
170
+ private querySnapshotToArray<T = any>(qs: QuerySnapshot): T[] {
171
+ const rows: any[] = []
172
+
173
+ qs.forEach(doc => {
174
+ rows.push({
175
+ id: unescapeDocId(doc.id),
176
+ ...doc.data(),
177
+ })
178
+ })
179
+
180
+ return rows
181
+ }
182
+
183
+ override async commitTransaction(_tx: DBTransaction, _opt?: CommonDBSaveOptions): Promise<void> {
184
+ throw new Error('commitTransaction is not supported yet')
185
+ }
186
+
187
+ override async ping(): Promise<void> {
188
+ // no-op now
189
+ }
190
+ }
@@ -0,0 +1,11 @@
1
+ import { replaceAll } from './string.util'
2
+
3
+ const SLASH = '_SLASH_'
4
+
5
+ export function escapeDocId(docId: string): string {
6
+ return replaceAll(docId, '/', SLASH)
7
+ }
8
+
9
+ export function unescapeDocId(docId: string): string {
10
+ return replaceAll(docId, SLASH, '/')
11
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ import {
2
+ FirestoreDB,
3
+ FirestoreDBCfg,
4
+ FirestoreDBOptions,
5
+ FirestoreDBSaveOptions,
6
+ } from './firestore.db'
7
+ import { dbQueryToFirestoreQuery } from './query.util'
8
+
9
+ export type { FirestoreDBCfg, FirestoreDBOptions, FirestoreDBSaveOptions }
10
+
11
+ export { FirestoreDB, dbQueryToFirestoreQuery }
@@ -0,0 +1,37 @@
1
+ import { Query, WhereFilterOp } from '@google-cloud/firestore'
2
+ import { DBQuery, DBQueryFilterOperator, ObjectWithId } from '@naturalcycles/db-lib'
3
+
4
+ // Map DBQueryFilterOp to WhereFilterOp
5
+ // Currently it's fully aligned!
6
+ const OP_MAP: Partial<Record<DBQueryFilterOperator, WhereFilterOp>> = {
7
+ // '=': '==',
8
+ // in: 'array-contains',
9
+ }
10
+
11
+ export function dbQueryToFirestoreQuery<ROW extends ObjectWithId>(
12
+ dbQuery: DBQuery<ROW>,
13
+ emptyQuery: Query,
14
+ ): Query {
15
+ // filter
16
+ // eslint-disable-next-line unicorn/no-array-reduce, unicorn/prefer-object-from-entries
17
+ let q = dbQuery._filters.reduce((q, f) => {
18
+ return q.where(f.name, OP_MAP[f.op] || (f.op as WhereFilterOp), f.val)
19
+ }, emptyQuery)
20
+
21
+ // order
22
+ // eslint-disable-next-line unicorn/no-array-reduce, unicorn/prefer-object-from-entries
23
+ q = dbQuery._orders.reduce((q, ord) => {
24
+ return q.orderBy(ord.name, ord.descending ? 'desc' : 'asc')
25
+ }, q)
26
+
27
+ // limit
28
+ q = q.limit(dbQuery._limitValue)
29
+
30
+ // selectedFields
31
+ if (dbQuery._selectedFieldNames) {
32
+ // todo: check if at least id / __key__ is required to be set
33
+ q = q.select(...dbQuery._selectedFieldNames)
34
+ }
35
+
36
+ return q
37
+ }
@@ -0,0 +1,4 @@
1
+ // todo: move to... js-lib? lodash-lib?
2
+ export function replaceAll(str: string, find: string, replace: string): string {
3
+ return str.replace(new RegExp(find, 'g'), replace)
4
+ }