@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 +105 -0
- package/dist/firestore.db.d.ts +27 -0
- package/dist/firestore.db.js +101 -0
- package/dist/firestore.util.d.ts +2 -0
- package/dist/firestore.util.js +13 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/query.util.d.ts +3 -0
- package/dist/query.util.js +30 -0
- package/dist/string.util.d.ts +1 -0
- package/dist/string.util.js +8 -0
- package/package.json +46 -0
- package/readme.md +17 -0
- package/src/firestore.db.ts +190 -0
- package/src/firestore.util.ts +11 -0
- package/src/index.ts +11 -0
- package/src/query.util.ts +37 -0
- package/src/string.util.ts +4 -0
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,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;
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
+
[](https://www.npmjs.com/package/@naturalcycles/firestore-lib)
|
|
6
|
+
[](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
|
+
}
|