@naturalcycles/db-lib 8.46.0 → 8.47.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/adapter/cachedb/cache.db.d.ts +2 -1
- package/dist/adapter/cachedb/cache.db.js +12 -0
- package/dist/adapter/inmemory/inMemory.db.d.ts +2 -1
- package/dist/adapter/inmemory/inMemory.db.js +27 -13
- package/dist/adapter/inmemory/queryInMemory.js +4 -0
- package/dist/base.common.db.d.ts +10 -9
- package/dist/base.common.db.js +28 -26
- package/dist/common.db.d.ts +20 -1
- package/dist/commondao/common.dao.d.ts +4 -1
- package/dist/commondao/common.dao.js +17 -1
- package/dist/db.model.d.ts +13 -0
- package/dist/db.model.js +17 -1
- package/dist/query/dbQuery.d.ts +17 -1
- package/dist/query/dbQuery.js +33 -1
- package/dist/testing/dbTest.d.ts +2 -0
- package/dist/testing/dbTest.js +43 -1
- package/package.json +2 -2
- package/src/adapter/cachedb/cache.db.ts +20 -1
- package/src/adapter/inmemory/inMemory.db.ts +34 -16
- package/src/adapter/inmemory/queryInMemory.ts +5 -0
- package/src/base.common.db.ts +40 -32
- package/src/common.db.ts +25 -0
- package/src/commondao/common.dao.ts +25 -1
- package/src/db.model.ts +19 -0
- package/src/query/dbQuery.ts +44 -2
- package/src/testing/dbTest.ts +59 -0
|
@@ -3,7 +3,7 @@ import { Readable } from 'node:stream';
|
|
|
3
3
|
import { JsonSchemaObject, JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib';
|
|
4
4
|
import { BaseCommonDB } from '../../base.common.db';
|
|
5
5
|
import { CommonDB } from '../../common.db';
|
|
6
|
-
import { CommonDBOptions, RunQueryResult } from '../../db.model';
|
|
6
|
+
import { CommonDBOptions, DBPatch, RunQueryResult } from '../../db.model';
|
|
7
7
|
import { DBQuery } from '../../query/dbQuery';
|
|
8
8
|
import { DBTransaction } from '../../transaction/dbTransaction';
|
|
9
9
|
import { CacheDBCfg, CacheDBCreateOptions, CacheDBOptions, CacheDBSaveOptions, CacheDBStreamOptions } from './cache.db.model';
|
|
@@ -30,5 +30,6 @@ export declare class CacheDB extends BaseCommonDB implements CommonDB {
|
|
|
30
30
|
runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CacheDBOptions): Promise<number>;
|
|
31
31
|
streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CacheDBStreamOptions): Readable;
|
|
32
32
|
deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CacheDBOptions): Promise<number>;
|
|
33
|
+
updateByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, patch: DBPatch<ROW>, opt?: CacheDBOptions): Promise<number>;
|
|
33
34
|
commitTransaction(tx: DBTransaction, opt?: CommonDBOptions): Promise<void>;
|
|
34
35
|
}
|
|
@@ -195,6 +195,18 @@ class CacheDB extends base_common_db_1.BaseCommonDB {
|
|
|
195
195
|
}
|
|
196
196
|
return deletedIds;
|
|
197
197
|
}
|
|
198
|
+
async updateByQuery(q, patch, opt = {}) {
|
|
199
|
+
let updated;
|
|
200
|
+
if (!opt.onlyCache && !this.cfg.onlyCache) {
|
|
201
|
+
updated = await this.cfg.downstreamDB.updateByQuery(q, patch, opt);
|
|
202
|
+
}
|
|
203
|
+
if (!opt.skipCache && !this.cfg.skipCache) {
|
|
204
|
+
const cacheResult = this.cfg.cacheDB.updateByQuery(q, patch, opt);
|
|
205
|
+
if (this.cfg.awaitCache)
|
|
206
|
+
updated ??= await cacheResult;
|
|
207
|
+
}
|
|
208
|
+
return updated || 0;
|
|
209
|
+
}
|
|
198
210
|
async commitTransaction(tx, opt) {
|
|
199
211
|
await this.cfg.downstreamDB.commitTransaction(tx, opt);
|
|
200
212
|
await this.cfg.cacheDB.commitTransaction(tx, opt);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { JsonSchemaObject, StringMap, JsonSchemaRootObject, ObjectWithId, CommonLogger } from '@naturalcycles/js-lib';
|
|
2
2
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib';
|
|
3
|
-
import { CommonDB, DBTransaction } from '../..';
|
|
3
|
+
import { CommonDB, DBPatch, DBTransaction } from '../..';
|
|
4
4
|
import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions, RunQueryResult } from '../../db.model';
|
|
5
5
|
import { DBQuery } from '../../query/dbQuery';
|
|
6
6
|
export interface InMemoryDBCfg {
|
|
@@ -56,6 +56,7 @@ export declare class InMemoryDB implements CommonDB {
|
|
|
56
56
|
saveBatch<ROW extends Partial<ObjectWithId>>(_table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
|
|
57
57
|
deleteByIds<ROW extends ObjectWithId>(_table: string, ids: ROW['id'][], _opt?: CommonDBOptions): Promise<number>;
|
|
58
58
|
deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<number>;
|
|
59
|
+
updateByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, patch: DBPatch<ROW>): Promise<number>;
|
|
59
60
|
runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<RunQueryResult<ROW>>;
|
|
60
61
|
runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<number>;
|
|
61
62
|
streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): ReadableTyped<ROW>;
|
|
@@ -94,14 +94,14 @@ class InMemoryDB {
|
|
|
94
94
|
async deleteByIds(_table, ids, _opt) {
|
|
95
95
|
const table = this.cfg.tablesPrefix + _table;
|
|
96
96
|
this.data[table] ||= {};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
let count = 0;
|
|
98
|
+
ids.forEach(id => {
|
|
99
|
+
if (!this.data[table][id])
|
|
100
|
+
return;
|
|
100
101
|
delete this.data[table][id];
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
.filter(Boolean).length;
|
|
102
|
+
count++;
|
|
103
|
+
});
|
|
104
|
+
return count;
|
|
105
105
|
}
|
|
106
106
|
async deleteByQuery(q, _opt) {
|
|
107
107
|
const table = this.cfg.tablesPrefix + q.table;
|
|
@@ -109,6 +109,24 @@ class InMemoryDB {
|
|
|
109
109
|
const ids = rows.map(r => r.id);
|
|
110
110
|
return await this.deleteByIds(q.table, ids);
|
|
111
111
|
}
|
|
112
|
+
async updateByQuery(q, patch) {
|
|
113
|
+
const patchEntries = Object.entries(patch);
|
|
114
|
+
if (!patchEntries.length)
|
|
115
|
+
return 0;
|
|
116
|
+
const table = this.cfg.tablesPrefix + q.table;
|
|
117
|
+
const rows = (0, __1.queryInMemory)(q, Object.values(this.data[table] || {}));
|
|
118
|
+
rows.forEach((row) => {
|
|
119
|
+
patchEntries.forEach(([k, v]) => {
|
|
120
|
+
if (v instanceof __1.DBIncrement) {
|
|
121
|
+
row[k] = (row[k] || 0) + v.amount;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
row[k] = v;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
return rows.length;
|
|
129
|
+
}
|
|
112
130
|
async runQuery(q, _opt) {
|
|
113
131
|
const table = this.cfg.tablesPrefix + q.table;
|
|
114
132
|
return { rows: (0, __1.queryInMemory)(q, Object.values(this.data[table] || {})) };
|
|
@@ -147,9 +165,7 @@ class InMemoryDB {
|
|
|
147
165
|
* Flushes all tables (all namespaces) at once.
|
|
148
166
|
*/
|
|
149
167
|
async flushToDisk() {
|
|
150
|
-
|
|
151
|
-
throw new Error('flushToDisk() called but persistenceEnabled=false');
|
|
152
|
-
}
|
|
168
|
+
(0, js_lib_1._assert)(this.cfg.persistenceEnabled, 'flushToDisk() called but persistenceEnabled=false');
|
|
153
169
|
const { persistentStoragePath, persistZip } = this.cfg;
|
|
154
170
|
const started = Date.now();
|
|
155
171
|
await fs.emptyDir(persistentStoragePath);
|
|
@@ -175,9 +191,7 @@ class InMemoryDB {
|
|
|
175
191
|
* Restores all tables (all namespaces) at once.
|
|
176
192
|
*/
|
|
177
193
|
async restoreFromDisk() {
|
|
178
|
-
|
|
179
|
-
throw new Error('restoreFromDisk() called but persistenceEnabled=false');
|
|
180
|
-
}
|
|
194
|
+
(0, js_lib_1._assert)(this.cfg.persistenceEnabled, 'restoreFromDisk() called but persistenceEnabled=false');
|
|
181
195
|
const { persistentStoragePath } = this.cfg;
|
|
182
196
|
const started = Date.now();
|
|
183
197
|
await fs.ensureDir(persistentStoragePath);
|
|
@@ -17,6 +17,10 @@ const FILTER_FNS = {
|
|
|
17
17
|
// Important: q.table is not used in this function, so tablesPrefix is not needed.
|
|
18
18
|
// But should be careful here..
|
|
19
19
|
function queryInMemory(q, rows = []) {
|
|
20
|
+
// .ids
|
|
21
|
+
if (q._ids?.length) {
|
|
22
|
+
rows = rows.filter(r => q._ids.includes(r.id));
|
|
23
|
+
}
|
|
20
24
|
// .filter
|
|
21
25
|
// eslint-disable-next-line unicorn/no-array-reduce
|
|
22
26
|
rows = q._filters.reduce((rows, filter) => {
|
package/dist/base.common.db.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { JsonSchemaObject, JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib';
|
|
2
2
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib';
|
|
3
3
|
import { CommonDB } from './common.db';
|
|
4
|
-
import { CommonDBOptions, CommonDBSaveOptions, RunQueryResult } from './db.model';
|
|
4
|
+
import { CommonDBOptions, CommonDBSaveOptions, DBPatch, RunQueryResult } from './db.model';
|
|
5
5
|
import { DBQuery } from './query/dbQuery';
|
|
6
6
|
import { DBTransaction } from './transaction/dbTransaction';
|
|
7
7
|
/**
|
|
@@ -12,14 +12,15 @@ export declare class BaseCommonDB implements CommonDB {
|
|
|
12
12
|
ping(): Promise<void>;
|
|
13
13
|
getTables(): Promise<string[]>;
|
|
14
14
|
getTableSchema<ROW extends ObjectWithId>(table: string): Promise<JsonSchemaRootObject<ROW>>;
|
|
15
|
-
createTable<ROW extends ObjectWithId>(
|
|
16
|
-
deleteByIds<ROW extends ObjectWithId>(
|
|
17
|
-
deleteByQuery<ROW extends ObjectWithId>(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
createTable<ROW extends ObjectWithId>(table: string, schema: JsonSchemaObject<ROW>): Promise<void>;
|
|
16
|
+
deleteByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][]): Promise<number>;
|
|
17
|
+
deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>): Promise<number>;
|
|
18
|
+
updateByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, patch: DBPatch<ROW>, opt?: CommonDBOptions): Promise<number>;
|
|
19
|
+
getByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][]): Promise<ROW[]>;
|
|
20
|
+
runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>): Promise<RunQueryResult<ROW>>;
|
|
21
|
+
runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>): Promise<number>;
|
|
22
|
+
saveBatch<ROW extends Partial<ObjectWithId>>(table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
|
|
23
|
+
streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>): ReadableTyped<ROW>;
|
|
23
24
|
/**
|
|
24
25
|
* Naive implementation.
|
|
25
26
|
* Doesn't support rollback on error, hence doesn't pass dbTest.
|
package/dist/base.common.db.js
CHANGED
|
@@ -1,45 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BaseCommonDB = void 0;
|
|
4
|
-
|
|
5
|
-
const dbTransaction_util_1 = require("./transaction/dbTransaction.util");
|
|
4
|
+
/* eslint-disable unused-imports/no-unused-vars */
|
|
6
5
|
/**
|
|
7
6
|
* No-op implementation of CommonDB interface.
|
|
8
7
|
* To be extended by actual implementations.
|
|
9
8
|
*/
|
|
10
9
|
class BaseCommonDB {
|
|
11
|
-
async ping() {
|
|
10
|
+
async ping() {
|
|
11
|
+
throw new Error('ping is not implemented');
|
|
12
|
+
}
|
|
12
13
|
async getTables() {
|
|
13
|
-
|
|
14
|
+
throw new Error('getTables is not implemented');
|
|
14
15
|
}
|
|
15
16
|
async getTableSchema(table) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
throw new Error('getTableSchema is not implemented');
|
|
18
|
+
}
|
|
19
|
+
async createTable(table, schema) {
|
|
20
|
+
throw new Error('createTable is not implemented');
|
|
21
|
+
}
|
|
22
|
+
async deleteByIds(table, ids) {
|
|
23
|
+
throw new Error('deleteByIds is not implemented');
|
|
24
|
+
}
|
|
25
|
+
async deleteByQuery(q) {
|
|
26
|
+
throw new Error('deleteByQuery is not implemented');
|
|
23
27
|
}
|
|
24
|
-
async
|
|
25
|
-
|
|
26
|
-
return 0;
|
|
28
|
+
async updateByQuery(q, patch, opt) {
|
|
29
|
+
throw new Error('updateByQuery is not implemented');
|
|
27
30
|
}
|
|
28
|
-
async
|
|
29
|
-
|
|
31
|
+
async getByIds(table, ids) {
|
|
32
|
+
throw new Error('getByIds is not implemented');
|
|
30
33
|
}
|
|
31
|
-
async
|
|
32
|
-
|
|
34
|
+
async runQuery(q) {
|
|
35
|
+
throw new Error('runQuery is not implemented');
|
|
33
36
|
}
|
|
34
|
-
async
|
|
35
|
-
|
|
37
|
+
async runQueryCount(q) {
|
|
38
|
+
throw new Error('runQueryCount is not implemented');
|
|
36
39
|
}
|
|
37
|
-
async
|
|
38
|
-
|
|
40
|
+
async saveBatch(table, rows, opt) {
|
|
41
|
+
throw new Error('saveBatch is not implemented');
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return node_stream_1.Readable.from([]);
|
|
43
|
+
streamQuery(q) {
|
|
44
|
+
throw new Error('streamQuery is not implemented');
|
|
43
45
|
}
|
|
44
46
|
/**
|
|
45
47
|
* Naive implementation.
|
|
@@ -47,7 +49,7 @@ class BaseCommonDB {
|
|
|
47
49
|
* To be extended.
|
|
48
50
|
*/
|
|
49
51
|
async commitTransaction(tx, opt) {
|
|
50
|
-
|
|
52
|
+
throw new Error('commitTransaction is not implemented');
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
exports.BaseCommonDB = BaseCommonDB;
|
package/dist/common.db.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { JsonSchemaObject, JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib';
|
|
2
2
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib';
|
|
3
|
-
import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, RunQueryResult } from './db.model';
|
|
3
|
+
import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, DBPatch, RunQueryResult } from './db.model';
|
|
4
4
|
import { DBQuery } from './query/dbQuery';
|
|
5
5
|
import { DBTransaction } from './transaction/dbTransaction';
|
|
6
6
|
export interface CommonDB {
|
|
@@ -48,6 +48,25 @@ export interface CommonDB {
|
|
|
48
48
|
*/
|
|
49
49
|
deleteByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][], opt?: CommonDBOptions): Promise<number>;
|
|
50
50
|
deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions): Promise<number>;
|
|
51
|
+
/**
|
|
52
|
+
* Applies patch to the rows returned by the query.
|
|
53
|
+
*
|
|
54
|
+
* Example:
|
|
55
|
+
*
|
|
56
|
+
* UPDATE table SET A = B where $QUERY_CONDITION
|
|
57
|
+
*
|
|
58
|
+
* patch would be { A: 'B' } for that query.
|
|
59
|
+
*
|
|
60
|
+
* Supports "increment query", example:
|
|
61
|
+
*
|
|
62
|
+
* UPDATE table SET A = A + 1
|
|
63
|
+
*
|
|
64
|
+
* In that case patch would look like:
|
|
65
|
+
* { A: DBIncrement(1) }
|
|
66
|
+
*
|
|
67
|
+
* Returns number of rows affected.
|
|
68
|
+
*/
|
|
69
|
+
updateByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, patch: DBPatch<ROW>, opt?: CommonDBOptions): Promise<number>;
|
|
51
70
|
/**
|
|
52
71
|
* Should be implemented as a Transaction (best effort), which means that
|
|
53
72
|
* either ALL or NONE of the operations should be applied.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AnyObject, AsyncMapper, JsonSchemaObject, JsonSchemaRootObject, ObjectWithId, Saved, Unsaved } from '@naturalcycles/js-lib';
|
|
2
2
|
import { AjvSchema, ObjectSchemaTyped, ReadableTyped } from '@naturalcycles/nodejs-lib';
|
|
3
|
-
import { DBDeleteByIdsOperation, DBModelType, DBOperation, DBSaveBatchOperation, RunQueryResult } from '../db.model';
|
|
3
|
+
import { DBDeleteByIdsOperation, DBModelType, DBOperation, DBPatch, DBSaveBatchOperation, RunQueryResult } from '../db.model';
|
|
4
4
|
import { DBQuery, RunnableDBQuery } from '../query/dbQuery';
|
|
5
5
|
import { CommonDaoCfg, CommonDaoCreateOptions, CommonDaoOptions, CommonDaoSaveOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions } from './common.dao.model';
|
|
6
6
|
/**
|
|
@@ -120,6 +120,9 @@ export declare class CommonDao<BM extends Partial<ObjectWithId<ID>>, DBM extends
|
|
|
120
120
|
deleteByQuery(q: DBQuery<DBM>, opt?: CommonDaoStreamForEachOptions<DBM> & {
|
|
121
121
|
stream?: boolean;
|
|
122
122
|
}): Promise<number>;
|
|
123
|
+
updateById(id: ID, patch: DBPatch<DBM>, opt?: CommonDaoOptions): Promise<number>;
|
|
124
|
+
updateByIds(ids: ID[], patch: DBPatch<DBM>, opt?: CommonDaoOptions): Promise<number>;
|
|
125
|
+
updateByQuery(q: DBQuery<DBM>, patch: DBPatch<DBM>, opt?: CommonDaoOptions): Promise<number>;
|
|
123
126
|
dbmToBM(_dbm: undefined, opt?: CommonDaoOptions): Promise<undefined>;
|
|
124
127
|
dbmToBM(_dbm?: DBM, opt?: CommonDaoOptions): Promise<Saved<BM>>;
|
|
125
128
|
dbmsToBM(dbms: DBM[], opt?: CommonDaoOptions): Promise<Saved<BM>[]>;
|
|
@@ -112,7 +112,7 @@ class CommonDao {
|
|
|
112
112
|
let dbm;
|
|
113
113
|
if (opt.timeout) {
|
|
114
114
|
// todo: possibly remove it after debugging is done
|
|
115
|
-
dbm = (await (0, js_lib_1.pTimeout)(this.cfg.db.getByIds(table, [id]), {
|
|
115
|
+
dbm = (await (0, js_lib_1.pTimeout)(() => this.cfg.db.getByIds(table, [id]), {
|
|
116
116
|
timeout: opt.timeout,
|
|
117
117
|
name: `getById(${table})`,
|
|
118
118
|
}))[0];
|
|
@@ -711,6 +711,22 @@ class CommonDao {
|
|
|
711
711
|
this.logSaveResult(started, op, q.table);
|
|
712
712
|
return deleted;
|
|
713
713
|
}
|
|
714
|
+
async updateById(id, patch, opt = {}) {
|
|
715
|
+
return await this.updateByQuery(this.query().byId(id), patch, opt);
|
|
716
|
+
}
|
|
717
|
+
async updateByIds(ids, patch, opt = {}) {
|
|
718
|
+
return await this.updateByQuery(this.query().byIds(ids), patch, opt);
|
|
719
|
+
}
|
|
720
|
+
async updateByQuery(q, patch, opt = {}) {
|
|
721
|
+
this.requireWriteAccess();
|
|
722
|
+
this.requireObjectMutability(opt);
|
|
723
|
+
q.table = opt.table || q.table;
|
|
724
|
+
const op = `updateByQuery(${q.pretty()})`;
|
|
725
|
+
const started = this.logStarted(op, q.table);
|
|
726
|
+
const updated = await this.cfg.db.updateByQuery(q, patch, opt);
|
|
727
|
+
this.logSaveResult(started, op, q.table);
|
|
728
|
+
return updated;
|
|
729
|
+
}
|
|
714
730
|
async dbmToBM(_dbm, opt = {}) {
|
|
715
731
|
if (!_dbm)
|
|
716
732
|
return;
|
package/dist/db.model.d.ts
CHANGED
|
@@ -62,3 +62,16 @@ export declare enum DBModelType {
|
|
|
62
62
|
BM = "BM",
|
|
63
63
|
TM = "TM"
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Allows to construct a query similar to:
|
|
67
|
+
*
|
|
68
|
+
* UPDATE table SET A = A + 1
|
|
69
|
+
*
|
|
70
|
+
* In this case DBIncement.of(1) will be needed.
|
|
71
|
+
*/
|
|
72
|
+
export declare class DBIncrement {
|
|
73
|
+
amount: number;
|
|
74
|
+
private constructor();
|
|
75
|
+
static of(amount: number): DBIncrement;
|
|
76
|
+
}
|
|
77
|
+
export type DBPatch<ROW extends Partial<ObjectWithId>> = Partial<Record<keyof ROW, ROW[keyof ROW] | DBIncrement>>;
|
package/dist/db.model.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DBModelType = exports.DBRelation = void 0;
|
|
3
|
+
exports.DBIncrement = exports.DBModelType = exports.DBRelation = void 0;
|
|
4
4
|
var DBRelation;
|
|
5
5
|
(function (DBRelation) {
|
|
6
6
|
DBRelation["ONE_TO_ONE"] = "ONE_TO_ONE";
|
|
@@ -12,3 +12,19 @@ var DBModelType;
|
|
|
12
12
|
DBModelType["BM"] = "BM";
|
|
13
13
|
DBModelType["TM"] = "TM";
|
|
14
14
|
})(DBModelType = exports.DBModelType || (exports.DBModelType = {}));
|
|
15
|
+
/**
|
|
16
|
+
* Allows to construct a query similar to:
|
|
17
|
+
*
|
|
18
|
+
* UPDATE table SET A = A + 1
|
|
19
|
+
*
|
|
20
|
+
* In this case DBIncement.of(1) will be needed.
|
|
21
|
+
*/
|
|
22
|
+
class DBIncrement {
|
|
23
|
+
constructor(amount) {
|
|
24
|
+
this.amount = amount;
|
|
25
|
+
}
|
|
26
|
+
static of(amount) {
|
|
27
|
+
return new DBIncrement(amount);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.DBIncrement = DBIncrement;
|
package/dist/query/dbQuery.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AnyObjectWithId, ObjectWithId, AsyncMapper, Saved, AnyObject } from '@naturalcycles/js-lib';
|
|
2
2
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib';
|
|
3
|
-
import { CommonDaoOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions } from '..';
|
|
3
|
+
import { CommonDaoOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions, DBPatch } from '..';
|
|
4
4
|
import { CommonDao } from '../commondao/common.dao';
|
|
5
5
|
import { RunQueryResult } from '../db.model';
|
|
6
6
|
/**
|
|
@@ -66,6 +66,7 @@ export declare class DBQuery<ROW extends ObjectWithId = AnyObjectWithId> {
|
|
|
66
66
|
_selectedFieldNames?: (keyof ROW)[];
|
|
67
67
|
_groupByFieldNames?: (keyof ROW)[];
|
|
68
68
|
_distinct: boolean;
|
|
69
|
+
_ids?: ROW['id'][];
|
|
69
70
|
filter(name: keyof ROW, op: DBQueryFilterOperator, val: any): this;
|
|
70
71
|
filterEq(name: keyof ROW, val: any): this;
|
|
71
72
|
limit(limit: number): this;
|
|
@@ -76,6 +77,20 @@ export declare class DBQuery<ROW extends ObjectWithId = AnyObjectWithId> {
|
|
|
76
77
|
distinct(distinct?: boolean): this;
|
|
77
78
|
startCursor(startCursor?: string): this;
|
|
78
79
|
endCursor(endCursor?: string): this;
|
|
80
|
+
/**
|
|
81
|
+
* Allows to query by ids (one or many).
|
|
82
|
+
* Similar to:
|
|
83
|
+
* SELECT * FROM table where id in (a, b, c)
|
|
84
|
+
* or (if only 1 id is passed)
|
|
85
|
+
* SELECT * FROM table where id = a
|
|
86
|
+
*/
|
|
87
|
+
byIds(ids: ROW['id'][]): this;
|
|
88
|
+
/**
|
|
89
|
+
* Allows to query by id.
|
|
90
|
+
* Similar to:
|
|
91
|
+
* SELECT * FROM table where id = a
|
|
92
|
+
*/
|
|
93
|
+
byId(id: ROW['id']): this;
|
|
79
94
|
clone(): DBQuery<ROW>;
|
|
80
95
|
pretty(): string;
|
|
81
96
|
prettyConditions(): string[];
|
|
@@ -97,6 +112,7 @@ export declare class RunnableDBQuery<BM extends Partial<ObjectWithId<ID>>, DBM e
|
|
|
97
112
|
runQueryExtendedAsDBM(opt?: CommonDaoOptions): Promise<RunQueryResult<DBM>>;
|
|
98
113
|
runQueryExtendedAsTM(opt?: CommonDaoOptions): Promise<RunQueryResult<TM>>;
|
|
99
114
|
runQueryCount(opt?: CommonDaoOptions): Promise<number>;
|
|
115
|
+
updateByQuery(patch: DBPatch<DBM>, opt?: CommonDaoOptions): Promise<number>;
|
|
100
116
|
streamQueryForEach(mapper: AsyncMapper<Saved<BM>, void>, opt?: CommonDaoStreamForEachOptions<Saved<BM>>): Promise<void>;
|
|
101
117
|
streamQueryAsDBMForEach(mapper: AsyncMapper<DBM, void>, opt?: CommonDaoStreamForEachOptions<DBM>): Promise<void>;
|
|
102
118
|
streamQuery(opt?: CommonDaoStreamOptions): ReadableTyped<Saved<BM>>;
|
package/dist/query/dbQuery.js
CHANGED
|
@@ -85,8 +85,28 @@ class DBQuery {
|
|
|
85
85
|
this._endCursor = endCursor;
|
|
86
86
|
return this;
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Allows to query by ids (one or many).
|
|
90
|
+
* Similar to:
|
|
91
|
+
* SELECT * FROM table where id in (a, b, c)
|
|
92
|
+
* or (if only 1 id is passed)
|
|
93
|
+
* SELECT * FROM table where id = a
|
|
94
|
+
*/
|
|
95
|
+
byIds(ids) {
|
|
96
|
+
this._ids = ids;
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Allows to query by id.
|
|
101
|
+
* Similar to:
|
|
102
|
+
* SELECT * FROM table where id = a
|
|
103
|
+
*/
|
|
104
|
+
byId(id) {
|
|
105
|
+
this._ids = [id];
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
88
108
|
clone() {
|
|
89
|
-
return
|
|
109
|
+
return (0, js_lib_1._objectAssign)(new DBQuery(this.table), {
|
|
90
110
|
_filters: [...this._filters],
|
|
91
111
|
_limitValue: this._limitValue,
|
|
92
112
|
_offsetValue: this._offsetValue,
|
|
@@ -96,6 +116,7 @@ class DBQuery {
|
|
|
96
116
|
_distinct: this._distinct,
|
|
97
117
|
_startCursor: this._startCursor,
|
|
98
118
|
_endCursor: this._endCursor,
|
|
119
|
+
_ids: this._ids,
|
|
99
120
|
});
|
|
100
121
|
}
|
|
101
122
|
pretty() {
|
|
@@ -109,6 +130,14 @@ class DBQuery {
|
|
|
109
130
|
if (this._selectedFieldNames) {
|
|
110
131
|
tokens.push(`select${this._distinct ? ' distinct' : ''}(${this._selectedFieldNames.join(',')})`);
|
|
111
132
|
}
|
|
133
|
+
if (this._ids?.length) {
|
|
134
|
+
if (this._ids.length === 1) {
|
|
135
|
+
tokens.push(`id=${this._ids[0]}`);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
tokens.push(`ids in (${this._ids.join(',')})`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
112
141
|
tokens.push(...this._filters.map(f => `${f.name}${f.op}${f.val}`), ...this._orders.map(o => `order by ${o.name}${o.descending ? ' desc' : ''}`));
|
|
113
142
|
if (this._groupByFieldNames) {
|
|
114
143
|
tokens.push(`groupBy(${this._groupByFieldNames.join(',')})`);
|
|
@@ -164,6 +193,9 @@ class RunnableDBQuery extends DBQuery {
|
|
|
164
193
|
async runQueryCount(opt) {
|
|
165
194
|
return await this.dao.runQueryCount(this, opt);
|
|
166
195
|
}
|
|
196
|
+
async updateByQuery(patch, opt) {
|
|
197
|
+
return await this.dao.updateByQuery(this, patch, opt);
|
|
198
|
+
}
|
|
167
199
|
async streamQueryForEach(mapper, opt) {
|
|
168
200
|
await this.dao.streamQueryForEach(this, mapper, opt);
|
|
169
201
|
}
|
package/dist/testing/dbTest.d.ts
CHANGED
package/dist/testing/dbTest.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.expectMatch = exports.runCommonDBTest = void 0;
|
|
4
4
|
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
5
5
|
const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
|
|
6
|
+
const db_model_1 = require("../db.model");
|
|
6
7
|
const dbQuery_1 = require("../query/dbQuery");
|
|
7
8
|
const dbTransaction_1 = require("../transaction/dbTransaction");
|
|
8
9
|
const test_model_1 = require("./test.model");
|
|
@@ -13,7 +14,7 @@ const test_util_1 = require("./test.util");
|
|
|
13
14
|
function runCommonDBTest(db, features = {}, quirks = {}) {
|
|
14
15
|
const { querying = true, tableSchemas = true, createTable = true, dbQueryFilter = true,
|
|
15
16
|
// dbQueryFilterIn = true,
|
|
16
|
-
dbQueryOrder = true, dbQuerySelectFields = true, insert = true, update = true, streaming = true, strongConsistency = true, bufferSupport = true, nullValues = true, documentDB = true, transactions = true, } = features;
|
|
17
|
+
dbQueryOrder = true, dbQuerySelectFields = true, insert = true, update = true, updateByQuery = true, dbIncrement = true, streaming = true, strongConsistency = true, bufferSupport = true, nullValues = true, documentDB = true, transactions = true, } = features;
|
|
17
18
|
// const {
|
|
18
19
|
// allowExtraPropertiesInResponse,
|
|
19
20
|
// allowBooleansAsUndefined,
|
|
@@ -245,6 +246,47 @@ function runCommonDBTest(db, features = {}, quirks = {}) {
|
|
|
245
246
|
expectMatch(expected, rows, quirks);
|
|
246
247
|
});
|
|
247
248
|
}
|
|
249
|
+
if (updateByQuery) {
|
|
250
|
+
// todo: query by ids (same as getByIds)
|
|
251
|
+
test('updateByQuery simple', async () => {
|
|
252
|
+
// cleanup, reset initial data
|
|
253
|
+
await db.deleteByQuery(queryAll());
|
|
254
|
+
await db.saveBatch(test_model_1.TEST_TABLE, items);
|
|
255
|
+
const patch = {
|
|
256
|
+
k3: 5,
|
|
257
|
+
k2: 'abc',
|
|
258
|
+
};
|
|
259
|
+
await db.updateByQuery(dbQuery_1.DBQuery.create(test_model_1.TEST_TABLE).filterEq('even', true), patch);
|
|
260
|
+
const { rows } = await db.runQuery(queryAll());
|
|
261
|
+
const expected = items.map(r => {
|
|
262
|
+
if (r.even) {
|
|
263
|
+
return { ...r, ...patch };
|
|
264
|
+
}
|
|
265
|
+
return r;
|
|
266
|
+
});
|
|
267
|
+
expectMatch(expected, rows, quirks);
|
|
268
|
+
});
|
|
269
|
+
if (dbIncrement) {
|
|
270
|
+
test('updateByQuery DBIncrement', async () => {
|
|
271
|
+
// cleanup, reset initial data
|
|
272
|
+
await db.deleteByQuery(queryAll());
|
|
273
|
+
await db.saveBatch(test_model_1.TEST_TABLE, items);
|
|
274
|
+
const patch = {
|
|
275
|
+
k3: db_model_1.DBIncrement.of(1),
|
|
276
|
+
k2: 'abcd',
|
|
277
|
+
};
|
|
278
|
+
await db.updateByQuery(dbQuery_1.DBQuery.create(test_model_1.TEST_TABLE).filterEq('even', true), patch);
|
|
279
|
+
const { rows } = await db.runQuery(queryAll());
|
|
280
|
+
const expected = items.map(r => {
|
|
281
|
+
if (r.even) {
|
|
282
|
+
return { ...r, ...patch, k3: (r.k3 || 0) + 1 };
|
|
283
|
+
}
|
|
284
|
+
return r;
|
|
285
|
+
});
|
|
286
|
+
expectMatch(expected, rows, quirks);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
248
290
|
if (querying) {
|
|
249
291
|
test('cleanup', async () => {
|
|
250
292
|
// CLEAN UP
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"prepare": "husky install"
|
|
5
5
|
},
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@naturalcycles/js-lib": "^14.
|
|
7
|
+
"@naturalcycles/js-lib": "^14.116.0",
|
|
8
8
|
"@naturalcycles/nodejs-lib": "^12.0.0",
|
|
9
9
|
"fs-extra": "^11.1.0"
|
|
10
10
|
},
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=14.15"
|
|
43
43
|
},
|
|
44
|
-
"version": "8.
|
|
44
|
+
"version": "8.47.0",
|
|
45
45
|
"description": "Lowest Common Denominator API to supported Databases",
|
|
46
46
|
"keywords": [
|
|
47
47
|
"db",
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from '@naturalcycles/js-lib'
|
|
8
8
|
import { BaseCommonDB } from '../../base.common.db'
|
|
9
9
|
import { CommonDB } from '../../common.db'
|
|
10
|
-
import { CommonDBOptions, RunQueryResult } from '../../db.model'
|
|
10
|
+
import { CommonDBOptions, DBPatch, RunQueryResult } from '../../db.model'
|
|
11
11
|
import { DBQuery } from '../../query/dbQuery'
|
|
12
12
|
import { DBTransaction } from '../../transaction/dbTransaction'
|
|
13
13
|
import {
|
|
@@ -294,6 +294,25 @@ export class CacheDB extends BaseCommonDB implements CommonDB {
|
|
|
294
294
|
return deletedIds
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
override async updateByQuery<ROW extends ObjectWithId>(
|
|
298
|
+
q: DBQuery<ROW>,
|
|
299
|
+
patch: DBPatch<ROW>,
|
|
300
|
+
opt: CacheDBOptions = {},
|
|
301
|
+
): Promise<number> {
|
|
302
|
+
let updated: number | undefined
|
|
303
|
+
|
|
304
|
+
if (!opt.onlyCache && !this.cfg.onlyCache) {
|
|
305
|
+
updated = await this.cfg.downstreamDB.updateByQuery(q, patch, opt)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!opt.skipCache && !this.cfg.skipCache) {
|
|
309
|
+
const cacheResult = this.cfg.cacheDB.updateByQuery(q, patch, opt)
|
|
310
|
+
if (this.cfg.awaitCache) updated ??= await cacheResult
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return updated || 0
|
|
314
|
+
}
|
|
315
|
+
|
|
297
316
|
override async commitTransaction(tx: DBTransaction, opt?: CommonDBOptions): Promise<void> {
|
|
298
317
|
await this.cfg.downstreamDB.commitTransaction(tx, opt)
|
|
299
318
|
await this.cfg.cacheDB.commitTransaction(tx, opt)
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
_stringMapValues,
|
|
14
14
|
CommonLogger,
|
|
15
15
|
_deepCopy,
|
|
16
|
+
_assert,
|
|
16
17
|
} from '@naturalcycles/js-lib'
|
|
17
18
|
import {
|
|
18
19
|
bufferReviver,
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
} from '@naturalcycles/nodejs-lib'
|
|
26
27
|
import { dimGrey, yellow } from '@naturalcycles/nodejs-lib/dist/colors'
|
|
27
28
|
import * as fs from 'fs-extra'
|
|
28
|
-
import { CommonDB, DBTransaction, queryInMemory } from '../..'
|
|
29
|
+
import { CommonDB, DBIncrement, DBPatch, DBTransaction, queryInMemory } from '../..'
|
|
29
30
|
import {
|
|
30
31
|
CommonDBCreateOptions,
|
|
31
32
|
CommonDBOptions,
|
|
@@ -149,7 +150,7 @@ export class InMemoryDB implements CommonDB {
|
|
|
149
150
|
): Promise<ROW[]> {
|
|
150
151
|
const table = this.cfg.tablesPrefix + _table
|
|
151
152
|
this.data[table] ||= {}
|
|
152
|
-
return ids.map(id => this.data[table]![id]).filter(Boolean)
|
|
153
|
+
return ids.map(id => this.data[table]![id] as ROW).filter(Boolean)
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
async saveBatch<ROW extends Partial<ObjectWithId>>(
|
|
@@ -190,14 +191,13 @@ export class InMemoryDB implements CommonDB {
|
|
|
190
191
|
): Promise<number> {
|
|
191
192
|
const table = this.cfg.tablesPrefix + _table
|
|
192
193
|
this.data[table] ||= {}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
.filter(Boolean).length
|
|
194
|
+
let count = 0
|
|
195
|
+
ids.forEach(id => {
|
|
196
|
+
if (!this.data[table]![id]) return
|
|
197
|
+
delete this.data[table]![id]
|
|
198
|
+
count++
|
|
199
|
+
})
|
|
200
|
+
return count
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
async deleteByQuery<ROW extends ObjectWithId>(
|
|
@@ -210,6 +210,28 @@ export class InMemoryDB implements CommonDB {
|
|
|
210
210
|
return await this.deleteByIds(q.table, ids)
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
async updateByQuery<ROW extends ObjectWithId>(
|
|
214
|
+
q: DBQuery<ROW>,
|
|
215
|
+
patch: DBPatch<ROW>,
|
|
216
|
+
): Promise<number> {
|
|
217
|
+
const patchEntries = Object.entries(patch)
|
|
218
|
+
if (!patchEntries.length) return 0
|
|
219
|
+
|
|
220
|
+
const table = this.cfg.tablesPrefix + q.table
|
|
221
|
+
const rows = queryInMemory(q, Object.values(this.data[table] || {}) as ROW[])
|
|
222
|
+
rows.forEach((row: any) => {
|
|
223
|
+
patchEntries.forEach(([k, v]) => {
|
|
224
|
+
if (v instanceof DBIncrement) {
|
|
225
|
+
row[k] = (row[k] || 0) + v.amount
|
|
226
|
+
} else {
|
|
227
|
+
row[k] = v
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
return rows.length
|
|
233
|
+
}
|
|
234
|
+
|
|
213
235
|
async runQuery<ROW extends ObjectWithId>(
|
|
214
236
|
q: DBQuery<ROW>,
|
|
215
237
|
_opt?: CommonDBOptions,
|
|
@@ -260,9 +282,7 @@ export class InMemoryDB implements CommonDB {
|
|
|
260
282
|
* Flushes all tables (all namespaces) at once.
|
|
261
283
|
*/
|
|
262
284
|
async flushToDisk(): Promise<void> {
|
|
263
|
-
|
|
264
|
-
throw new Error('flushToDisk() called but persistenceEnabled=false')
|
|
265
|
-
}
|
|
285
|
+
_assert(this.cfg.persistenceEnabled, 'flushToDisk() called but persistenceEnabled=false')
|
|
266
286
|
const { persistentStoragePath, persistZip } = this.cfg
|
|
267
287
|
|
|
268
288
|
const started = Date.now()
|
|
@@ -297,9 +317,7 @@ export class InMemoryDB implements CommonDB {
|
|
|
297
317
|
* Restores all tables (all namespaces) at once.
|
|
298
318
|
*/
|
|
299
319
|
async restoreFromDisk(): Promise<void> {
|
|
300
|
-
|
|
301
|
-
throw new Error('restoreFromDisk() called but persistenceEnabled=false')
|
|
302
|
-
}
|
|
320
|
+
_assert(this.cfg.persistenceEnabled, 'restoreFromDisk() called but persistenceEnabled=false')
|
|
303
321
|
const { persistentStoragePath } = this.cfg
|
|
304
322
|
|
|
305
323
|
const started = Date.now()
|
|
@@ -19,6 +19,11 @@ const FILTER_FNS: Record<DBQueryFilterOperator, FilterFn> = {
|
|
|
19
19
|
// Important: q.table is not used in this function, so tablesPrefix is not needed.
|
|
20
20
|
// But should be careful here..
|
|
21
21
|
export function queryInMemory<ROW extends ObjectWithId>(q: DBQuery<ROW>, rows: ROW[] = []): ROW[] {
|
|
22
|
+
// .ids
|
|
23
|
+
if (q._ids?.length) {
|
|
24
|
+
rows = rows.filter(r => q._ids!.includes(r.id))
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
// .filter
|
|
23
28
|
// eslint-disable-next-line unicorn/no-array-reduce
|
|
24
29
|
rows = q._filters.reduce((rows, filter) => {
|
package/src/base.common.db.ts
CHANGED
|
@@ -1,68 +1,76 @@
|
|
|
1
|
-
import { Readable } from 'node:stream'
|
|
2
1
|
import { JsonSchemaObject, JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib'
|
|
3
2
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib'
|
|
4
3
|
import { CommonDB } from './common.db'
|
|
5
|
-
import { CommonDBOptions, CommonDBSaveOptions, RunQueryResult } from './db.model'
|
|
4
|
+
import { CommonDBOptions, CommonDBSaveOptions, DBPatch, RunQueryResult } from './db.model'
|
|
6
5
|
import { DBQuery } from './query/dbQuery'
|
|
7
6
|
import { DBTransaction } from './transaction/dbTransaction'
|
|
8
|
-
|
|
7
|
+
|
|
8
|
+
/* eslint-disable unused-imports/no-unused-vars */
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* No-op implementation of CommonDB interface.
|
|
12
12
|
* To be extended by actual implementations.
|
|
13
13
|
*/
|
|
14
14
|
export class BaseCommonDB implements CommonDB {
|
|
15
|
-
async ping(): Promise<void> {
|
|
15
|
+
async ping(): Promise<void> {
|
|
16
|
+
throw new Error('ping is not implemented')
|
|
17
|
+
}
|
|
16
18
|
|
|
17
19
|
async getTables(): Promise<string[]> {
|
|
18
|
-
|
|
20
|
+
throw new Error('getTables is not implemented')
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
async getTableSchema<ROW extends ObjectWithId>(
|
|
22
24
|
table: string,
|
|
23
25
|
): Promise<JsonSchemaRootObject<ROW>> {
|
|
24
|
-
|
|
25
|
-
$id: `${table}.schema.json`,
|
|
26
|
-
type: 'object',
|
|
27
|
-
additionalProperties: true,
|
|
28
|
-
properties: {} as any,
|
|
29
|
-
required: [],
|
|
30
|
-
}
|
|
26
|
+
throw new Error('getTableSchema is not implemented')
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
async createTable<ROW extends ObjectWithId>(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
): Promise<void> {
|
|
30
|
+
table: string,
|
|
31
|
+
schema: JsonSchemaObject<ROW>,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
throw new Error('createTable is not implemented')
|
|
34
|
+
}
|
|
37
35
|
|
|
38
|
-
async deleteByIds<ROW extends ObjectWithId>(
|
|
39
|
-
|
|
36
|
+
async deleteByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][]): Promise<number> {
|
|
37
|
+
throw new Error('deleteByIds is not implemented')
|
|
40
38
|
}
|
|
41
39
|
|
|
42
|
-
async deleteByQuery<ROW extends ObjectWithId>(
|
|
43
|
-
|
|
40
|
+
async deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>): Promise<number> {
|
|
41
|
+
throw new Error('deleteByQuery is not implemented')
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
async
|
|
47
|
-
|
|
44
|
+
async updateByQuery<ROW extends ObjectWithId>(
|
|
45
|
+
q: DBQuery<ROW>,
|
|
46
|
+
patch: DBPatch<ROW>,
|
|
47
|
+
opt?: CommonDBOptions,
|
|
48
|
+
): Promise<number> {
|
|
49
|
+
throw new Error('updateByQuery is not implemented')
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
async
|
|
51
|
-
|
|
52
|
+
async getByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][]): Promise<ROW[]> {
|
|
53
|
+
throw new Error('getByIds is not implemented')
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
async
|
|
55
|
-
|
|
56
|
+
async runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>): Promise<RunQueryResult<ROW>> {
|
|
57
|
+
throw new Error('runQuery is not implemented')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>): Promise<number> {
|
|
61
|
+
throw new Error('runQueryCount is not implemented')
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
async saveBatch<ROW extends Partial<ObjectWithId>>(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
): Promise<void> {
|
|
65
|
+
table: string,
|
|
66
|
+
rows: ROW[],
|
|
67
|
+
opt?: CommonDBSaveOptions<ROW>,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
throw new Error('saveBatch is not implemented')
|
|
70
|
+
}
|
|
63
71
|
|
|
64
|
-
streamQuery<ROW extends ObjectWithId>(
|
|
65
|
-
|
|
72
|
+
streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>): ReadableTyped<ROW> {
|
|
73
|
+
throw new Error('streamQuery is not implemented')
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
/**
|
|
@@ -71,6 +79,6 @@ export class BaseCommonDB implements CommonDB {
|
|
|
71
79
|
* To be extended.
|
|
72
80
|
*/
|
|
73
81
|
async commitTransaction(tx: DBTransaction, opt?: CommonDBOptions): Promise<void> {
|
|
74
|
-
|
|
82
|
+
throw new Error('commitTransaction is not implemented')
|
|
75
83
|
}
|
|
76
84
|
}
|
package/src/common.db.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
CommonDBOptions,
|
|
6
6
|
CommonDBSaveOptions,
|
|
7
7
|
CommonDBStreamOptions,
|
|
8
|
+
DBPatch,
|
|
8
9
|
RunQueryResult,
|
|
9
10
|
} from './db.model'
|
|
10
11
|
import { DBQuery } from './query/dbQuery'
|
|
@@ -92,6 +93,30 @@ export interface CommonDB {
|
|
|
92
93
|
|
|
93
94
|
deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions): Promise<number>
|
|
94
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Applies patch to the rows returned by the query.
|
|
98
|
+
*
|
|
99
|
+
* Example:
|
|
100
|
+
*
|
|
101
|
+
* UPDATE table SET A = B where $QUERY_CONDITION
|
|
102
|
+
*
|
|
103
|
+
* patch would be { A: 'B' } for that query.
|
|
104
|
+
*
|
|
105
|
+
* Supports "increment query", example:
|
|
106
|
+
*
|
|
107
|
+
* UPDATE table SET A = A + 1
|
|
108
|
+
*
|
|
109
|
+
* In that case patch would look like:
|
|
110
|
+
* { A: DBIncrement(1) }
|
|
111
|
+
*
|
|
112
|
+
* Returns number of rows affected.
|
|
113
|
+
*/
|
|
114
|
+
updateByQuery<ROW extends ObjectWithId>(
|
|
115
|
+
q: DBQuery<ROW>,
|
|
116
|
+
patch: DBPatch<ROW>,
|
|
117
|
+
opt?: CommonDBOptions,
|
|
118
|
+
): Promise<number>
|
|
119
|
+
|
|
95
120
|
// TRANSACTION
|
|
96
121
|
/**
|
|
97
122
|
* Should be implemented as a Transaction (best effort), which means that
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
DBDeleteByIdsOperation,
|
|
41
41
|
DBModelType,
|
|
42
42
|
DBOperation,
|
|
43
|
+
DBPatch,
|
|
43
44
|
DBSaveBatchOperation,
|
|
44
45
|
RunQueryResult,
|
|
45
46
|
} from '../db.model'
|
|
@@ -134,7 +135,7 @@ export class CommonDao<
|
|
|
134
135
|
if (opt.timeout) {
|
|
135
136
|
// todo: possibly remove it after debugging is done
|
|
136
137
|
dbm = (
|
|
137
|
-
await pTimeout(this.cfg.db.getByIds<DBM>(table, [id]), {
|
|
138
|
+
await pTimeout(() => this.cfg.db.getByIds<DBM>(table, [id]), {
|
|
138
139
|
timeout: opt.timeout,
|
|
139
140
|
name: `getById(${table})`,
|
|
140
141
|
})
|
|
@@ -934,6 +935,29 @@ export class CommonDao<
|
|
|
934
935
|
return deleted
|
|
935
936
|
}
|
|
936
937
|
|
|
938
|
+
async updateById(id: ID, patch: DBPatch<DBM>, opt: CommonDaoOptions = {}): Promise<number> {
|
|
939
|
+
return await this.updateByQuery(this.query().byId(id), patch, opt)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async updateByIds(ids: ID[], patch: DBPatch<DBM>, opt: CommonDaoOptions = {}): Promise<number> {
|
|
943
|
+
return await this.updateByQuery(this.query().byIds(ids), patch, opt)
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async updateByQuery(
|
|
947
|
+
q: DBQuery<DBM>,
|
|
948
|
+
patch: DBPatch<DBM>,
|
|
949
|
+
opt: CommonDaoOptions = {},
|
|
950
|
+
): Promise<number> {
|
|
951
|
+
this.requireWriteAccess()
|
|
952
|
+
this.requireObjectMutability(opt)
|
|
953
|
+
q.table = opt.table || q.table
|
|
954
|
+
const op = `updateByQuery(${q.pretty()})`
|
|
955
|
+
const started = this.logStarted(op, q.table)
|
|
956
|
+
const updated = await this.cfg.db.updateByQuery(q, patch, opt)
|
|
957
|
+
this.logSaveResult(started, op, q.table)
|
|
958
|
+
return updated
|
|
959
|
+
}
|
|
960
|
+
|
|
937
961
|
// CONVERSIONS
|
|
938
962
|
|
|
939
963
|
async dbmToBM(_dbm: undefined, opt?: CommonDaoOptions): Promise<undefined>
|
package/src/db.model.ts
CHANGED
|
@@ -76,3 +76,22 @@ export enum DBModelType {
|
|
|
76
76
|
BM = 'BM',
|
|
77
77
|
TM = 'TM',
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Allows to construct a query similar to:
|
|
82
|
+
*
|
|
83
|
+
* UPDATE table SET A = A + 1
|
|
84
|
+
*
|
|
85
|
+
* In this case DBIncement.of(1) will be needed.
|
|
86
|
+
*/
|
|
87
|
+
export class DBIncrement {
|
|
88
|
+
private constructor(public amount: number) {}
|
|
89
|
+
|
|
90
|
+
static of(amount: number): DBIncrement {
|
|
91
|
+
return new DBIncrement(amount)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type DBPatch<ROW extends Partial<ObjectWithId>> = Partial<
|
|
96
|
+
Record<keyof ROW, ROW[keyof ROW] | DBIncrement>
|
|
97
|
+
>
|
package/src/query/dbQuery.ts
CHANGED
|
@@ -5,9 +5,15 @@ import {
|
|
|
5
5
|
_truncate,
|
|
6
6
|
Saved,
|
|
7
7
|
AnyObject,
|
|
8
|
+
_objectAssign,
|
|
8
9
|
} from '@naturalcycles/js-lib'
|
|
9
10
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib'
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
CommonDaoOptions,
|
|
13
|
+
CommonDaoStreamForEachOptions,
|
|
14
|
+
CommonDaoStreamOptions,
|
|
15
|
+
DBPatch,
|
|
16
|
+
} from '..'
|
|
11
17
|
import { CommonDao } from '../commondao/common.dao'
|
|
12
18
|
import { RunQueryResult } from '../db.model'
|
|
13
19
|
|
|
@@ -107,6 +113,7 @@ export class DBQuery<ROW extends ObjectWithId = AnyObjectWithId> {
|
|
|
107
113
|
_selectedFieldNames?: (keyof ROW)[]
|
|
108
114
|
_groupByFieldNames?: (keyof ROW)[]
|
|
109
115
|
_distinct = false
|
|
116
|
+
_ids?: ROW['id'][]
|
|
110
117
|
|
|
111
118
|
filter(name: keyof ROW, op: DBQueryFilterOperator, val: any): this {
|
|
112
119
|
this._filters.push({ name, op, val })
|
|
@@ -161,8 +168,30 @@ export class DBQuery<ROW extends ObjectWithId = AnyObjectWithId> {
|
|
|
161
168
|
return this
|
|
162
169
|
}
|
|
163
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Allows to query by ids (one or many).
|
|
173
|
+
* Similar to:
|
|
174
|
+
* SELECT * FROM table where id in (a, b, c)
|
|
175
|
+
* or (if only 1 id is passed)
|
|
176
|
+
* SELECT * FROM table where id = a
|
|
177
|
+
*/
|
|
178
|
+
byIds(ids: ROW['id'][]): this {
|
|
179
|
+
this._ids = ids
|
|
180
|
+
return this
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Allows to query by id.
|
|
185
|
+
* Similar to:
|
|
186
|
+
* SELECT * FROM table where id = a
|
|
187
|
+
*/
|
|
188
|
+
byId(id: ROW['id']): this {
|
|
189
|
+
this._ids = [id]
|
|
190
|
+
return this
|
|
191
|
+
}
|
|
192
|
+
|
|
164
193
|
clone(): DBQuery<ROW> {
|
|
165
|
-
return
|
|
194
|
+
return _objectAssign(new DBQuery<ROW>(this.table), {
|
|
166
195
|
_filters: [...this._filters],
|
|
167
196
|
_limitValue: this._limitValue,
|
|
168
197
|
_offsetValue: this._offsetValue,
|
|
@@ -172,6 +201,7 @@ export class DBQuery<ROW extends ObjectWithId = AnyObjectWithId> {
|
|
|
172
201
|
_distinct: this._distinct,
|
|
173
202
|
_startCursor: this._startCursor,
|
|
174
203
|
_endCursor: this._endCursor,
|
|
204
|
+
_ids: this._ids,
|
|
175
205
|
})
|
|
176
206
|
}
|
|
177
207
|
|
|
@@ -192,6 +222,14 @@ export class DBQuery<ROW extends ObjectWithId = AnyObjectWithId> {
|
|
|
192
222
|
)
|
|
193
223
|
}
|
|
194
224
|
|
|
225
|
+
if (this._ids?.length) {
|
|
226
|
+
if (this._ids.length === 1) {
|
|
227
|
+
tokens.push(`id=${this._ids[0]}`)
|
|
228
|
+
} else {
|
|
229
|
+
tokens.push(`ids in (${this._ids.join(',')})`)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
195
233
|
tokens.push(
|
|
196
234
|
...this._filters.map(f => `${f.name as string}${f.op}${f.val}`),
|
|
197
235
|
...this._orders.map(o => `order by ${o.name as string}${o.descending ? ' desc' : ''}`),
|
|
@@ -269,6 +307,10 @@ export class RunnableDBQuery<
|
|
|
269
307
|
return await this.dao.runQueryCount(this, opt)
|
|
270
308
|
}
|
|
271
309
|
|
|
310
|
+
async updateByQuery(patch: DBPatch<DBM>, opt?: CommonDaoOptions): Promise<number> {
|
|
311
|
+
return await this.dao.updateByQuery(this, patch, opt)
|
|
312
|
+
}
|
|
313
|
+
|
|
272
314
|
async streamQueryForEach(
|
|
273
315
|
mapper: AsyncMapper<Saved<BM>, void>,
|
|
274
316
|
opt?: CommonDaoStreamForEachOptions<Saved<BM>>,
|
package/src/testing/dbTest.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { pDelay, pMap, _filterObject, _pick, _sortBy } from '@naturalcycles/js-lib'
|
|
2
2
|
import { readableToArray } from '@naturalcycles/nodejs-lib'
|
|
3
3
|
import { CommonDB } from '../common.db'
|
|
4
|
+
import { DBIncrement, DBPatch } from '../db.model'
|
|
4
5
|
import { DBQuery } from '../query/dbQuery'
|
|
5
6
|
import { DBTransaction } from '../transaction/dbTransaction'
|
|
6
7
|
import {
|
|
@@ -25,6 +26,10 @@ export interface CommonDBImplementationFeatures {
|
|
|
25
26
|
insert?: boolean
|
|
26
27
|
update?: boolean
|
|
27
28
|
|
|
29
|
+
updateByQuery?: boolean
|
|
30
|
+
|
|
31
|
+
dbIncrement?: boolean
|
|
32
|
+
|
|
28
33
|
createTable?: boolean
|
|
29
34
|
tableSchemas?: boolean
|
|
30
35
|
|
|
@@ -87,6 +92,8 @@ export function runCommonDBTest(
|
|
|
87
92
|
dbQuerySelectFields = true,
|
|
88
93
|
insert = true,
|
|
89
94
|
update = true,
|
|
95
|
+
updateByQuery = true,
|
|
96
|
+
dbIncrement = true,
|
|
90
97
|
streaming = true,
|
|
91
98
|
strongConsistency = true,
|
|
92
99
|
bufferSupport = true,
|
|
@@ -383,6 +390,58 @@ export function runCommonDBTest(
|
|
|
383
390
|
})
|
|
384
391
|
}
|
|
385
392
|
|
|
393
|
+
if (updateByQuery) {
|
|
394
|
+
// todo: query by ids (same as getByIds)
|
|
395
|
+
test('updateByQuery simple', async () => {
|
|
396
|
+
// cleanup, reset initial data
|
|
397
|
+
await db.deleteByQuery(queryAll())
|
|
398
|
+
await db.saveBatch(TEST_TABLE, items)
|
|
399
|
+
|
|
400
|
+
const patch: DBPatch<TestItemDBM> = {
|
|
401
|
+
k3: 5,
|
|
402
|
+
k2: 'abc',
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
await db.updateByQuery(DBQuery.create<TestItemDBM>(TEST_TABLE).filterEq('even', true), patch)
|
|
406
|
+
|
|
407
|
+
const { rows } = await db.runQuery(queryAll())
|
|
408
|
+
const expected = items.map(r => {
|
|
409
|
+
if (r.even) {
|
|
410
|
+
return { ...r, ...patch }
|
|
411
|
+
}
|
|
412
|
+
return r
|
|
413
|
+
})
|
|
414
|
+
expectMatch(expected, rows, quirks)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
if (dbIncrement) {
|
|
418
|
+
test('updateByQuery DBIncrement', async () => {
|
|
419
|
+
// cleanup, reset initial data
|
|
420
|
+
await db.deleteByQuery(queryAll())
|
|
421
|
+
await db.saveBatch(TEST_TABLE, items)
|
|
422
|
+
|
|
423
|
+
const patch: DBPatch<TestItemDBM> = {
|
|
424
|
+
k3: DBIncrement.of(1),
|
|
425
|
+
k2: 'abcd',
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
await db.updateByQuery(
|
|
429
|
+
DBQuery.create<TestItemDBM>(TEST_TABLE).filterEq('even', true),
|
|
430
|
+
patch,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
const { rows } = await db.runQuery(queryAll())
|
|
434
|
+
const expected = items.map(r => {
|
|
435
|
+
if (r.even) {
|
|
436
|
+
return { ...r, ...patch, k3: (r.k3 || 0) + 1 }
|
|
437
|
+
}
|
|
438
|
+
return r
|
|
439
|
+
})
|
|
440
|
+
expectMatch(expected, rows, quirks)
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
386
445
|
if (querying) {
|
|
387
446
|
test('cleanup', async () => {
|
|
388
447
|
// CLEAN UP
|