@naturalcycles/db-lib 9.22.0 → 9.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/inmemory/inMemoryKeyValueDB.d.ts +8 -9
- package/dist/adapter/inmemory/inMemoryKeyValueDB.js +8 -20
- package/dist/kv/commonKeyValueDB.d.ts +13 -19
- package/dist/kv/commonKeyValueDao.d.ts +17 -25
- package/dist/kv/commonKeyValueDao.js +32 -54
- package/dist/kv/commonKeyValueDaoMemoCache.d.ts +2 -2
- package/dist/testing/keyValueDBTest.js +15 -12
- package/dist/testing/keyValueDaoTest.d.ts +2 -2
- package/dist/testing/keyValueDaoTest.js +31 -8
- package/package.json +1 -1
- package/src/adapter/inmemory/inMemoryKeyValueDB.ts +16 -32
- package/src/kv/commonKeyValueDB.ts +14 -21
- package/src/kv/commonKeyValueDao.ts +53 -86
- package/src/kv/commonKeyValueDaoMemoCache.ts +2 -2
- package/src/testing/keyValueDBTest.ts +22 -19
- package/src/testing/keyValueDaoTest.ts +37 -12
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { StringMap } from '@naturalcycles/js-lib';
|
|
1
|
+
import { KeyValueTuple, StringMap } from '@naturalcycles/js-lib';
|
|
2
2
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib';
|
|
3
3
|
import { CommonDBCreateOptions } from '../../db.model';
|
|
4
|
-
import { CommonKeyValueDB,
|
|
4
|
+
import { CommonKeyValueDB, IncrementTuple } from '../../kv/commonKeyValueDB';
|
|
5
5
|
export interface InMemoryKeyValueDBCfg {
|
|
6
6
|
}
|
|
7
7
|
export declare class InMemoryKeyValueDB implements CommonKeyValueDB {
|
|
@@ -11,16 +11,15 @@ export declare class InMemoryKeyValueDB implements CommonKeyValueDB {
|
|
|
11
11
|
count?: boolean;
|
|
12
12
|
increment?: boolean;
|
|
13
13
|
};
|
|
14
|
-
data: StringMap<StringMap<
|
|
14
|
+
data: StringMap<StringMap<any>>;
|
|
15
15
|
ping(): Promise<void>;
|
|
16
16
|
createTable(_table: string, _opt?: CommonDBCreateOptions): Promise<void>;
|
|
17
17
|
deleteByIds(table: string, ids: string[]): Promise<void>;
|
|
18
|
-
getByIds(table: string, ids: string[]): Promise<
|
|
19
|
-
saveBatch(table: string, entries:
|
|
18
|
+
getByIds<V>(table: string, ids: string[]): Promise<KeyValueTuple<string, V>[]>;
|
|
19
|
+
saveBatch<V>(table: string, entries: KeyValueTuple<string, V>[]): Promise<void>;
|
|
20
20
|
streamIds(table: string, limit?: number): ReadableTyped<string>;
|
|
21
|
-
streamValues(table: string, limit?: number): ReadableTyped<
|
|
22
|
-
streamEntries(table: string, limit?: number): ReadableTyped<
|
|
21
|
+
streamValues<V>(table: string, limit?: number): ReadableTyped<V>;
|
|
22
|
+
streamEntries<V>(table: string, limit?: number): ReadableTyped<KeyValueTuple<string, V>>;
|
|
23
23
|
count(table: string): Promise<number>;
|
|
24
|
-
|
|
25
|
-
incrementBatch(table: string, incrementMap: StringMap<number>): Promise<StringMap<number>>;
|
|
24
|
+
incrementBatch(table: string, entries: IncrementTuple[]): Promise<IncrementTuple[]>;
|
|
26
25
|
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.InMemoryKeyValueDB = void 0;
|
|
4
4
|
const node_stream_1 = require("node:stream");
|
|
5
|
-
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
6
5
|
const commonKeyValueDB_1 = require("../../kv/commonKeyValueDB");
|
|
7
6
|
class InMemoryKeyValueDB {
|
|
8
7
|
constructor(cfg = {}) {
|
|
@@ -10,7 +9,7 @@ class InMemoryKeyValueDB {
|
|
|
10
9
|
this.support = {
|
|
11
10
|
...commonKeyValueDB_1.commonKeyValueDBFullSupport,
|
|
12
11
|
};
|
|
13
|
-
// data[table][id]
|
|
12
|
+
// data[table][id] => V
|
|
14
13
|
this.data = {};
|
|
15
14
|
}
|
|
16
15
|
async ping() { }
|
|
@@ -19,14 +18,13 @@ class InMemoryKeyValueDB {
|
|
|
19
18
|
this.data[table] ||= {};
|
|
20
19
|
ids.forEach(id => delete this.data[table][id]);
|
|
21
20
|
}
|
|
22
|
-
// todo: but should we work with Tuples or Objects?
|
|
23
21
|
async getByIds(table, ids) {
|
|
24
22
|
this.data[table] ||= {};
|
|
25
23
|
return ids.map(id => [id, this.data[table][id]]).filter(e => e[1]);
|
|
26
24
|
}
|
|
27
25
|
async saveBatch(table, entries) {
|
|
28
26
|
this.data[table] ||= {};
|
|
29
|
-
entries.forEach(([id,
|
|
27
|
+
entries.forEach(([id, v]) => (this.data[table][id] = v));
|
|
30
28
|
}
|
|
31
29
|
streamIds(table, limit) {
|
|
32
30
|
return node_stream_1.Readable.from(Object.keys(this.data[table] || {}).slice(0, limit));
|
|
@@ -41,23 +39,13 @@ class InMemoryKeyValueDB {
|
|
|
41
39
|
this.data[table] ||= {};
|
|
42
40
|
return Object.keys(this.data[table]).length;
|
|
43
41
|
}
|
|
44
|
-
async
|
|
42
|
+
async incrementBatch(table, entries) {
|
|
45
43
|
this.data[table] ||= {};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
async incrementBatch(table, incrementMap) {
|
|
52
|
-
this.data[table] ||= {};
|
|
53
|
-
const result = {};
|
|
54
|
-
for (const [id, by] of (0, js_lib_1._stringMapEntries)(incrementMap)) {
|
|
55
|
-
const newValue = parseInt(this.data[table][id]?.toString() || '0') + by;
|
|
56
|
-
// todo: but should this.data store Buffer or number for incremented values?
|
|
57
|
-
this.data[table][id] = Buffer.from(String(newValue));
|
|
58
|
-
result[id] = newValue;
|
|
59
|
-
}
|
|
60
|
-
return result;
|
|
44
|
+
return entries.map(([id, by]) => {
|
|
45
|
+
const newValue = (this.data[table][id] || 0) + by;
|
|
46
|
+
this.data[table][id] = newValue;
|
|
47
|
+
return [id, newValue];
|
|
48
|
+
});
|
|
61
49
|
}
|
|
62
50
|
}
|
|
63
51
|
exports.InMemoryKeyValueDB = InMemoryKeyValueDB;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Integer, KeyValueTuple, UnixTimestampNumber } from '@naturalcycles/js-lib';
|
|
2
2
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib';
|
|
3
3
|
import { CommonDBCreateOptions } from '../db.model';
|
|
4
4
|
/**
|
|
@@ -25,37 +25,31 @@ export interface CommonKeyValueDB {
|
|
|
25
25
|
*
|
|
26
26
|
* Currently it is NOT required to maintain the same order as input `ids`.
|
|
27
27
|
*/
|
|
28
|
-
getByIds: (table: string, ids: string[]) => Promise<
|
|
28
|
+
getByIds: <V>(table: string, ids: string[]) => Promise<KeyValueTuple<string, V>[]>;
|
|
29
29
|
deleteByIds: (table: string, ids: string[]) => Promise<void>;
|
|
30
|
-
saveBatch: (table: string, entries:
|
|
30
|
+
saveBatch: <V>(table: string, entries: KeyValueTuple<string, V>[], opt?: CommonKeyValueDBSaveBatchOptions) => Promise<void>;
|
|
31
31
|
streamIds: (table: string, limit?: number) => ReadableTyped<string>;
|
|
32
|
-
streamValues: (table: string, limit?: number) => ReadableTyped<
|
|
33
|
-
streamEntries: (table: string, limit?: number) => ReadableTyped<
|
|
32
|
+
streamValues: <V>(table: string, limit?: number) => ReadableTyped<V>;
|
|
33
|
+
streamEntries: <V>(table: string, limit?: number) => ReadableTyped<KeyValueTuple<string, V>>;
|
|
34
34
|
count: (table: string) => Promise<number>;
|
|
35
|
-
/**
|
|
36
|
-
* Increments the value of a key in a table by a given amount.
|
|
37
|
-
* Default increment is 1 when `by` is not provided.
|
|
38
|
-
*
|
|
39
|
-
* Returns the new value.
|
|
40
|
-
*
|
|
41
|
-
* @experimental
|
|
42
|
-
*/
|
|
43
|
-
increment: (table: string, id: string, by?: number) => Promise<number>;
|
|
44
35
|
/**
|
|
45
36
|
* Perform a batch of Increment operations.
|
|
46
|
-
* Given
|
|
37
|
+
* Given entries array, increment each key of it (1st index of the tuple) by the given amount (2nd index of the tuple).
|
|
47
38
|
*
|
|
48
39
|
* Example:
|
|
49
|
-
*
|
|
40
|
+
* [
|
|
41
|
+
* ['key1', 2],
|
|
42
|
+
* ['key2', 3],
|
|
43
|
+
* ]
|
|
50
44
|
* would increment `key1` by 2, and `key2` by 3.
|
|
51
45
|
*
|
|
52
|
-
* Returns the
|
|
46
|
+
* Returns the entries array with tuples of the same structure, with updated numbers.
|
|
53
47
|
*
|
|
54
48
|
* @experimental
|
|
55
49
|
*/
|
|
56
|
-
incrementBatch: (table: string,
|
|
50
|
+
incrementBatch: (table: string, entries: IncrementTuple[]) => Promise<IncrementTuple[]>;
|
|
57
51
|
}
|
|
58
|
-
export type
|
|
52
|
+
export type IncrementTuple = [key: string, value: Integer];
|
|
59
53
|
export interface CommonKeyValueDBSaveBatchOptions {
|
|
60
54
|
/**
|
|
61
55
|
* If set (and if it's implemented by the driver) - will set expiry TTL for each key of the batch.
|
|
@@ -2,8 +2,8 @@ import { CommonLogger, KeyValueTuple } from '@naturalcycles/js-lib';
|
|
|
2
2
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib';
|
|
3
3
|
import { CommonDaoLogLevel } from '../commondao/common.dao.model';
|
|
4
4
|
import { CommonDBCreateOptions } from '../db.model';
|
|
5
|
-
import { CommonKeyValueDB, CommonKeyValueDBSaveBatchOptions,
|
|
6
|
-
export interface CommonKeyValueDaoCfg<V> {
|
|
5
|
+
import { CommonKeyValueDB, CommonKeyValueDBSaveBatchOptions, IncrementTuple } from './commonKeyValueDB';
|
|
6
|
+
export interface CommonKeyValueDaoCfg<RAW_V, V = RAW_V> {
|
|
7
7
|
db: CommonKeyValueDB;
|
|
8
8
|
table: string;
|
|
9
9
|
/**
|
|
@@ -23,40 +23,31 @@ export interface CommonKeyValueDaoCfg<V> {
|
|
|
23
23
|
* @default false
|
|
24
24
|
*/
|
|
25
25
|
logStarted?: boolean;
|
|
26
|
-
|
|
27
|
-
mapValueToBuffer?: (v: V) => Promise<Buffer>;
|
|
28
|
-
mapBufferToValue?: (b: Buffer) => Promise<V>;
|
|
29
|
-
beforeCreate?: (v: Partial<V>) => Partial<V>;
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Set to `true` to conveniently enable zipping+JSON.stringify
|
|
33
|
-
* (and unzipping+JSON.parse) of the Buffer value via hooks.
|
|
34
|
-
* Custom hooks will override these hooks (if provided).
|
|
35
|
-
*/
|
|
36
|
-
deflatedJsonValue?: boolean;
|
|
26
|
+
transformer?: CommonKeyValueDaoTransformer<V, RAW_V>;
|
|
37
27
|
}
|
|
38
28
|
export type CommonKeyValueDaoSaveOptions = CommonKeyValueDBSaveBatchOptions;
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
export interface CommonKeyValueDaoTransformer<V, RAW_V> {
|
|
30
|
+
valueToRaw: (v: V) => Promise<RAW_V>;
|
|
31
|
+
rawToValue: (raw: RAW_V) => Promise<V>;
|
|
32
|
+
}
|
|
33
|
+
export declare const commonKeyValueDaoDeflatedJsonTransformer: CommonKeyValueDaoTransformer<any, Buffer>;
|
|
34
|
+
export declare class CommonKeyValueDao<K extends string, RAW_V, V = RAW_V> {
|
|
35
|
+
constructor(cfg: CommonKeyValueDaoCfg<RAW_V, V>);
|
|
36
|
+
cfg: CommonKeyValueDaoCfg<RAW_V, V> & {
|
|
43
37
|
logger: CommonLogger;
|
|
44
38
|
};
|
|
45
39
|
ping(): Promise<void>;
|
|
46
40
|
createTable(opt?: CommonDBCreateOptions): Promise<void>;
|
|
47
|
-
create(input?: Partial<V>): V;
|
|
48
41
|
getById(id?: K): Promise<V | null>;
|
|
49
|
-
|
|
42
|
+
getByIdRaw(id?: K): Promise<RAW_V | null>;
|
|
50
43
|
requireById(id: K): Promise<V>;
|
|
51
|
-
|
|
52
|
-
getByIdOrEmpty(id: K, part?: Partial<V>): Promise<V>;
|
|
53
|
-
patch(id: K, patch: Partial<V>, opt?: CommonKeyValueDaoSaveOptions): Promise<V>;
|
|
44
|
+
requireByIdRaw(id: K): Promise<RAW_V>;
|
|
54
45
|
getByIds(ids: K[]): Promise<KeyValueTuple<string, V>[]>;
|
|
55
|
-
|
|
46
|
+
getByIdsRaw(ids: K[]): Promise<KeyValueTuple<K, RAW_V>[]>;
|
|
56
47
|
save(id: K, value: V, opt?: CommonKeyValueDaoSaveOptions): Promise<void>;
|
|
57
|
-
|
|
48
|
+
saveRaw(id: K, value: RAW_V, opt?: CommonKeyValueDaoSaveOptions): Promise<void>;
|
|
58
49
|
saveBatch(entries: KeyValueTuple<K, V>[], opt?: CommonKeyValueDaoSaveOptions): Promise<void>;
|
|
59
|
-
|
|
50
|
+
saveBatchRaw(entries: KeyValueTuple<K, RAW_V>[], opt?: CommonKeyValueDaoSaveOptions): Promise<void>;
|
|
60
51
|
deleteByIds(ids: K[]): Promise<void>;
|
|
61
52
|
deleteById(id: K): Promise<void>;
|
|
62
53
|
streamIds(limit?: number): ReadableTyped<K>;
|
|
@@ -72,4 +63,5 @@ export declare class CommonKeyValueDao<V, K extends string = string> {
|
|
|
72
63
|
* Returns the new value of the field.
|
|
73
64
|
*/
|
|
74
65
|
increment(id: K, by?: number): Promise<number>;
|
|
66
|
+
incrementBatch(entries: IncrementTuple[]): Promise<IncrementTuple[]>;
|
|
75
67
|
}
|
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CommonKeyValueDao = void 0;
|
|
3
|
+
exports.CommonKeyValueDao = exports.commonKeyValueDaoDeflatedJsonTransformer = void 0;
|
|
4
4
|
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
5
5
|
const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
|
|
6
|
+
exports.commonKeyValueDaoDeflatedJsonTransformer = {
|
|
7
|
+
valueToRaw: async (v) => await (0, nodejs_lib_1.deflateString)(JSON.stringify(v)),
|
|
8
|
+
rawToValue: async (raw) => JSON.parse(await (0, nodejs_lib_1.inflateToString)(raw)),
|
|
9
|
+
};
|
|
6
10
|
// todo: logging
|
|
7
11
|
// todo: readonly
|
|
8
12
|
class CommonKeyValueDao {
|
|
9
13
|
constructor(cfg) {
|
|
10
14
|
this.cfg = {
|
|
11
|
-
hooks: {},
|
|
12
15
|
logger: console,
|
|
13
16
|
...cfg,
|
|
14
17
|
};
|
|
15
|
-
if (cfg.deflatedJsonValue) {
|
|
16
|
-
this.cfg.hooks = {
|
|
17
|
-
mapValueToBuffer: async (v) => await (0, nodejs_lib_1.deflateString)(JSON.stringify(v)),
|
|
18
|
-
mapBufferToValue: async (buf) => JSON.parse(await (0, nodejs_lib_1.inflateToString)(buf)),
|
|
19
|
-
...cfg.hooks,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
18
|
}
|
|
23
19
|
async ping() {
|
|
24
20
|
await this.cfg.db.ping();
|
|
@@ -26,18 +22,13 @@ class CommonKeyValueDao {
|
|
|
26
22
|
async createTable(opt = {}) {
|
|
27
23
|
await this.cfg.db.createTable(this.cfg.table, opt);
|
|
28
24
|
}
|
|
29
|
-
create(input = {}) {
|
|
30
|
-
return {
|
|
31
|
-
...this.cfg.hooks.beforeCreate?.(input),
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
25
|
async getById(id) {
|
|
35
26
|
if (!id)
|
|
36
27
|
return null;
|
|
37
28
|
const [r] = await this.getByIds([id]);
|
|
38
29
|
return r?.[1] || null;
|
|
39
30
|
}
|
|
40
|
-
async
|
|
31
|
+
async getByIdRaw(id) {
|
|
41
32
|
if (!id)
|
|
42
33
|
return null;
|
|
43
34
|
const [r] = await this.cfg.db.getByIds(this.cfg.table, [id]);
|
|
@@ -54,7 +45,7 @@ class CommonKeyValueDao {
|
|
|
54
45
|
}
|
|
55
46
|
return r[1];
|
|
56
47
|
}
|
|
57
|
-
async
|
|
48
|
+
async requireByIdRaw(id) {
|
|
58
49
|
const [r] = await this.cfg.db.getByIds(this.cfg.table, [id]);
|
|
59
50
|
if (!r) {
|
|
60
51
|
const { table } = this.cfg;
|
|
@@ -65,53 +56,36 @@ class CommonKeyValueDao {
|
|
|
65
56
|
}
|
|
66
57
|
return r[1];
|
|
67
58
|
}
|
|
68
|
-
async getByIdOrEmpty(id, part = {}) {
|
|
69
|
-
const [r] = await this.getByIds([id]);
|
|
70
|
-
if (r)
|
|
71
|
-
return r[1];
|
|
72
|
-
return {
|
|
73
|
-
...this.cfg.hooks.beforeCreate?.({}),
|
|
74
|
-
...part,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
async patch(id, patch, opt) {
|
|
78
|
-
const v = {
|
|
79
|
-
...(await this.getByIdOrEmpty(id)),
|
|
80
|
-
...patch,
|
|
81
|
-
};
|
|
82
|
-
await this.save(id, v, opt);
|
|
83
|
-
return v;
|
|
84
|
-
}
|
|
85
59
|
async getByIds(ids) {
|
|
86
60
|
const entries = await this.cfg.db.getByIds(this.cfg.table, ids);
|
|
87
|
-
if (!this.cfg.
|
|
61
|
+
if (!this.cfg.transformer)
|
|
88
62
|
return entries;
|
|
89
|
-
return await (0, js_lib_1.pMap)(entries, async ([id,
|
|
63
|
+
return await (0, js_lib_1.pMap)(entries, async ([id, raw]) => [
|
|
90
64
|
id,
|
|
91
|
-
await this.cfg.
|
|
65
|
+
await this.cfg.transformer.rawToValue(raw),
|
|
92
66
|
]);
|
|
93
67
|
}
|
|
94
|
-
async
|
|
68
|
+
async getByIdsRaw(ids) {
|
|
95
69
|
return (await this.cfg.db.getByIds(this.cfg.table, ids));
|
|
96
70
|
}
|
|
97
71
|
async save(id, value, opt) {
|
|
98
72
|
await this.saveBatch([[id, value]], opt);
|
|
99
73
|
}
|
|
100
|
-
async
|
|
74
|
+
async saveRaw(id, value, opt) {
|
|
101
75
|
await this.cfg.db.saveBatch(this.cfg.table, [[id, value]], opt);
|
|
102
76
|
}
|
|
103
77
|
async saveBatch(entries, opt) {
|
|
104
|
-
const {
|
|
105
|
-
let
|
|
106
|
-
if (!
|
|
107
|
-
|
|
78
|
+
const { transformer } = this.cfg;
|
|
79
|
+
let rawEntries;
|
|
80
|
+
if (!transformer) {
|
|
81
|
+
rawEntries = entries;
|
|
108
82
|
}
|
|
109
83
|
else {
|
|
110
|
-
|
|
84
|
+
rawEntries = await (0, js_lib_1.pMap)(entries, async ([id, v]) => [id, await transformer.valueToRaw(v)]);
|
|
111
85
|
}
|
|
112
|
-
await this.cfg.db.saveBatch(this.cfg.table,
|
|
86
|
+
await this.cfg.db.saveBatch(this.cfg.table, rawEntries, opt);
|
|
113
87
|
}
|
|
114
|
-
async
|
|
88
|
+
async saveBatchRaw(entries, opt) {
|
|
115
89
|
await this.cfg.db.saveBatch(this.cfg.table, entries, opt);
|
|
116
90
|
}
|
|
117
91
|
async deleteByIds(ids) {
|
|
@@ -124,13 +98,13 @@ class CommonKeyValueDao {
|
|
|
124
98
|
return this.cfg.db.streamIds(this.cfg.table, limit);
|
|
125
99
|
}
|
|
126
100
|
streamValues(limit) {
|
|
127
|
-
const {
|
|
128
|
-
if (!
|
|
101
|
+
const { transformer } = this.cfg;
|
|
102
|
+
if (!transformer) {
|
|
129
103
|
return this.cfg.db.streamValues(this.cfg.table, limit);
|
|
130
104
|
}
|
|
131
|
-
return this.cfg.db.streamValues(this.cfg.table, limit).flatMap(async (
|
|
105
|
+
return this.cfg.db.streamValues(this.cfg.table, limit).flatMap(async (raw) => {
|
|
132
106
|
try {
|
|
133
|
-
return [await
|
|
107
|
+
return [await transformer.rawToValue(raw)];
|
|
134
108
|
}
|
|
135
109
|
catch (err) {
|
|
136
110
|
this.cfg.logger.error(err);
|
|
@@ -141,13 +115,13 @@ class CommonKeyValueDao {
|
|
|
141
115
|
});
|
|
142
116
|
}
|
|
143
117
|
streamEntries(limit) {
|
|
144
|
-
const {
|
|
145
|
-
if (!
|
|
118
|
+
const { transformer } = this.cfg;
|
|
119
|
+
if (!transformer) {
|
|
146
120
|
return this.cfg.db.streamEntries(this.cfg.table, limit);
|
|
147
121
|
}
|
|
148
|
-
return this.cfg.db.streamEntries(this.cfg.table, limit).flatMap(async ([id,
|
|
122
|
+
return this.cfg.db.streamEntries(this.cfg.table, limit).flatMap(async ([id, raw]) => {
|
|
149
123
|
try {
|
|
150
|
-
return [[id, await
|
|
124
|
+
return [[id, await transformer.rawToValue(raw)]];
|
|
151
125
|
}
|
|
152
126
|
catch (err) {
|
|
153
127
|
this.cfg.logger.error(err);
|
|
@@ -173,7 +147,11 @@ class CommonKeyValueDao {
|
|
|
173
147
|
* Returns the new value of the field.
|
|
174
148
|
*/
|
|
175
149
|
async increment(id, by = 1) {
|
|
176
|
-
|
|
150
|
+
const [t] = await this.cfg.db.incrementBatch(this.cfg.table, [[id, by]]);
|
|
151
|
+
return t[1];
|
|
152
|
+
}
|
|
153
|
+
async incrementBatch(entries) {
|
|
154
|
+
return await this.cfg.db.incrementBatch(this.cfg.table, entries);
|
|
177
155
|
}
|
|
178
156
|
}
|
|
179
157
|
exports.CommonKeyValueDao = CommonKeyValueDao;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AsyncMemoCache, MISS, NumberOfSeconds } from '@naturalcycles/js-lib';
|
|
2
2
|
import { CommonKeyValueDao } from './commonKeyValueDao';
|
|
3
3
|
export interface CommonKeyValueDaoMemoCacheCfg<VALUE> {
|
|
4
|
-
dao: CommonKeyValueDao<VALUE>;
|
|
4
|
+
dao: CommonKeyValueDao<string, VALUE>;
|
|
5
5
|
/**
|
|
6
6
|
* If set, every `set()` will set `expireAt` (TTL) option.
|
|
7
7
|
*/
|
|
@@ -15,7 +15,7 @@ export interface CommonKeyValueDaoMemoCacheCfg<VALUE> {
|
|
|
15
15
|
* Also, does not support .clear(), as it's more dangerous than useful to actually
|
|
16
16
|
* clear the whole table/cache.
|
|
17
17
|
*/
|
|
18
|
-
export declare class CommonKeyValueDaoMemoCache<VALUE
|
|
18
|
+
export declare class CommonKeyValueDaoMemoCache<VALUE> implements AsyncMemoCache<string, VALUE> {
|
|
19
19
|
private cfg;
|
|
20
20
|
constructor(cfg: CommonKeyValueDaoMemoCacheCfg<VALUE>);
|
|
21
21
|
get(k: string): Promise<VALUE | typeof MISS>;
|
|
@@ -4,7 +4,10 @@ exports.runCommonKeyValueDBTest = runCommonKeyValueDBTest;
|
|
|
4
4
|
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
5
5
|
const test_model_1 = require("./test.model");
|
|
6
6
|
const testIds = (0, js_lib_1._range)(1, 4).map(n => `id${n}`);
|
|
7
|
-
const testEntries = testIds.map(id => [
|
|
7
|
+
const testEntries = testIds.map(id => [
|
|
8
|
+
id,
|
|
9
|
+
Buffer.from(`${id}value`),
|
|
10
|
+
]);
|
|
8
11
|
function runCommonKeyValueDBTest(db) {
|
|
9
12
|
beforeAll(async () => {
|
|
10
13
|
// Tests in this suite are not isolated,
|
|
@@ -89,23 +92,23 @@ function runCommonKeyValueDBTest(db) {
|
|
|
89
92
|
const id = 'nonExistingField';
|
|
90
93
|
const id2 = 'nonExistingField2';
|
|
91
94
|
test('increment on a non-existing field should set the value to 1', async () => {
|
|
92
|
-
const result = await db.
|
|
93
|
-
expect(result).
|
|
95
|
+
const result = await db.incrementBatch(test_model_1.TEST_TABLE, [[id, 1]]);
|
|
96
|
+
expect(result).toEqual([[id, 1]]);
|
|
94
97
|
});
|
|
95
98
|
test('increment on a existing field should increase the value by one', async () => {
|
|
96
|
-
const result = await db.
|
|
97
|
-
expect(result).
|
|
99
|
+
const result = await db.incrementBatch(test_model_1.TEST_TABLE, [[id, 1]]);
|
|
100
|
+
expect(result).toEqual([[id, 2]]);
|
|
98
101
|
});
|
|
99
102
|
test('increment should increase the value by the specified amount', async () => {
|
|
100
|
-
const result = await db.
|
|
101
|
-
expect(result).
|
|
103
|
+
const result = await db.incrementBatch(test_model_1.TEST_TABLE, [[id, 2]]);
|
|
104
|
+
expect(result).toEqual([[id, 4]]);
|
|
102
105
|
});
|
|
103
106
|
test('increment 2 ids at the same time', async () => {
|
|
104
|
-
const result = await db.incrementBatch(test_model_1.TEST_TABLE,
|
|
105
|
-
[id
|
|
106
|
-
[id2
|
|
107
|
-
|
|
108
|
-
expect(result).toEqual({
|
|
107
|
+
const result = await db.incrementBatch(test_model_1.TEST_TABLE, [
|
|
108
|
+
[id, 1],
|
|
109
|
+
[id2, 2],
|
|
110
|
+
]);
|
|
111
|
+
expect(Object.fromEntries(result)).toEqual({
|
|
109
112
|
[id]: 5,
|
|
110
113
|
[id2]: 2,
|
|
111
114
|
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function runCommonKeyValueDaoTest(
|
|
1
|
+
import { CommonKeyValueDB } from '../kv/commonKeyValueDB';
|
|
2
|
+
export declare function runCommonKeyValueDaoTest(db: CommonKeyValueDB): void;
|
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runCommonKeyValueDaoTest = runCommonKeyValueDaoTest;
|
|
4
4
|
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
5
|
+
const commonKeyValueDao_1 = require("../kv/commonKeyValueDao");
|
|
6
|
+
const test_model_1 = require("./test.model");
|
|
7
|
+
const testItems = (0, test_model_1.createTestItemsBM)(4);
|
|
8
|
+
const testIds = testItems.map(e => e.id);
|
|
9
|
+
const testEntries = testItems.map(e => [e.id, e]);
|
|
10
|
+
function runCommonKeyValueDaoTest(db) {
|
|
11
|
+
const dao = new commonKeyValueDao_1.CommonKeyValueDao({
|
|
12
|
+
db,
|
|
13
|
+
table: test_model_1.TEST_TABLE,
|
|
14
|
+
// todo: make this test support "deflatedJson" transformer
|
|
15
|
+
});
|
|
16
|
+
const { support } = db;
|
|
8
17
|
beforeAll(async () => {
|
|
9
18
|
// Tests in this suite are not isolated,
|
|
10
19
|
// and failing tests can leave the DB in an unexpected state for other tests,
|
|
@@ -17,7 +26,6 @@ function runCommonKeyValueDaoTest(dao) {
|
|
|
17
26
|
const ids = await dao.streamIds().toArray();
|
|
18
27
|
await dao.deleteByIds(ids);
|
|
19
28
|
});
|
|
20
|
-
const { support } = dao.cfg.db;
|
|
21
29
|
test('ping', async () => {
|
|
22
30
|
await dao.ping();
|
|
23
31
|
});
|
|
@@ -34,8 +42,11 @@ function runCommonKeyValueDaoTest(dao) {
|
|
|
34
42
|
test('saveBatch, then getByIds', async () => {
|
|
35
43
|
await dao.saveBatch(testEntries);
|
|
36
44
|
const entries = await dao.getByIds(testIds);
|
|
45
|
+
// console.log(typeof entries[0]![1], entries[0]![1])
|
|
37
46
|
(0, js_lib_1._sortBy)(entries, e => e[0], true);
|
|
38
|
-
expect(entries).toEqual(testEntries);
|
|
47
|
+
expect(entries).toEqual(testEntries); // Jest doesn't allow to compare Buffers directly
|
|
48
|
+
// expect(entries.map(e => e[0])).toEqual(testEntries.map(e => e[0]))
|
|
49
|
+
// expect(entries.map(e => e[1].toString())).toEqual(testEntries.map(e => e[1].toString()))
|
|
39
50
|
});
|
|
40
51
|
test('streamIds', async () => {
|
|
41
52
|
const ids = await dao.streamIds().toArray();
|
|
@@ -77,17 +88,29 @@ function runCommonKeyValueDaoTest(dao) {
|
|
|
77
88
|
expect(results).toEqual([]);
|
|
78
89
|
});
|
|
79
90
|
if (support.increment) {
|
|
91
|
+
const id = 'nonExistingField';
|
|
92
|
+
const id2 = 'nonExistingField2';
|
|
80
93
|
test('increment on a non-existing field should set the value to 1', async () => {
|
|
81
|
-
const result = await dao.increment(
|
|
94
|
+
const result = await dao.increment(id);
|
|
82
95
|
expect(result).toBe(1);
|
|
83
96
|
});
|
|
84
97
|
test('increment on a existing field should increase the value by one', async () => {
|
|
85
|
-
const result = await dao.increment(
|
|
98
|
+
const result = await dao.increment(id);
|
|
86
99
|
expect(result).toBe(2);
|
|
87
100
|
});
|
|
88
101
|
test('increment should increase the value by the specified amount', async () => {
|
|
89
|
-
const result = await dao.increment(
|
|
102
|
+
const result = await dao.increment(id, 2);
|
|
90
103
|
expect(result).toBe(4);
|
|
91
104
|
});
|
|
105
|
+
test('increment 2 ids at the same time', async () => {
|
|
106
|
+
const result = await dao.incrementBatch([
|
|
107
|
+
[id, 1],
|
|
108
|
+
[id2, 2],
|
|
109
|
+
]);
|
|
110
|
+
expect(Object.fromEntries(result)).toEqual({
|
|
111
|
+
[id]: 5,
|
|
112
|
+
[id2]: 2,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
92
115
|
}
|
|
93
116
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Readable } from 'node:stream'
|
|
2
|
-
import {
|
|
2
|
+
import { KeyValueTuple, StringMap } from '@naturalcycles/js-lib'
|
|
3
3
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib'
|
|
4
4
|
import { CommonDBCreateOptions } from '../../db.model'
|
|
5
5
|
import {
|
|
6
6
|
CommonKeyValueDB,
|
|
7
7
|
commonKeyValueDBFullSupport,
|
|
8
|
-
|
|
8
|
+
IncrementTuple,
|
|
9
9
|
} from '../../kv/commonKeyValueDB'
|
|
10
10
|
|
|
11
11
|
export interface InMemoryKeyValueDBCfg {}
|
|
@@ -17,8 +17,8 @@ export class InMemoryKeyValueDB implements CommonKeyValueDB {
|
|
|
17
17
|
...commonKeyValueDBFullSupport,
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
// data[table][id]
|
|
21
|
-
data: StringMap<StringMap<
|
|
20
|
+
// data[table][id] => V
|
|
21
|
+
data: StringMap<StringMap<any>> = {}
|
|
22
22
|
|
|
23
23
|
async ping(): Promise<void> {}
|
|
24
24
|
|
|
@@ -29,26 +29,25 @@ export class InMemoryKeyValueDB implements CommonKeyValueDB {
|
|
|
29
29
|
ids.forEach(id => delete this.data[table]![id])
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
async getByIds(table: string, ids: string[]): Promise<KeyValueDBTuple[]> {
|
|
32
|
+
async getByIds<V>(table: string, ids: string[]): Promise<KeyValueTuple<string, V>[]> {
|
|
34
33
|
this.data[table] ||= {}
|
|
35
|
-
return ids.map(id => [id, this.data[table]![id]!] as
|
|
34
|
+
return ids.map(id => [id, this.data[table]![id]!] as KeyValueTuple<string, V>).filter(e => e[1])
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
async saveBatch(table: string, entries:
|
|
37
|
+
async saveBatch<V>(table: string, entries: KeyValueTuple<string, V>[]): Promise<void> {
|
|
39
38
|
this.data[table] ||= {}
|
|
40
|
-
entries.forEach(([id,
|
|
39
|
+
entries.forEach(([id, v]) => (this.data[table]![id] = v))
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
streamIds(table: string, limit?: number): ReadableTyped<string> {
|
|
44
43
|
return Readable.from(Object.keys(this.data[table] || {}).slice(0, limit))
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
streamValues(table: string, limit?: number): ReadableTyped<
|
|
46
|
+
streamValues<V>(table: string, limit?: number): ReadableTyped<V> {
|
|
48
47
|
return Readable.from(Object.values(this.data[table] || {}).slice(0, limit))
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
streamEntries(table: string, limit?: number): ReadableTyped<
|
|
50
|
+
streamEntries<V>(table: string, limit?: number): ReadableTyped<KeyValueTuple<string, V>> {
|
|
52
51
|
return Readable.from(Object.entries(this.data[table] || {}).slice(0, limit))
|
|
53
52
|
}
|
|
54
53
|
|
|
@@ -57,28 +56,13 @@ export class InMemoryKeyValueDB implements CommonKeyValueDB {
|
|
|
57
56
|
return Object.keys(this.data[table]).length
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
async
|
|
59
|
+
async incrementBatch(table: string, entries: IncrementTuple[]): Promise<IncrementTuple[]> {
|
|
61
60
|
this.data[table] ||= {}
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async incrementBatch(table: string, incrementMap: StringMap<number>): Promise<StringMap<number>> {
|
|
71
|
-
this.data[table] ||= {}
|
|
72
|
-
|
|
73
|
-
const result: StringMap<number> = {}
|
|
74
|
-
|
|
75
|
-
for (const [id, by] of _stringMapEntries(incrementMap)) {
|
|
76
|
-
const newValue = parseInt(this.data[table][id]?.toString() || '0') + by
|
|
77
|
-
// todo: but should this.data store Buffer or number for incremented values?
|
|
78
|
-
this.data[table][id] = Buffer.from(String(newValue))
|
|
79
|
-
result[id] = newValue
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return result
|
|
62
|
+
return entries.map(([id, by]) => {
|
|
63
|
+
const newValue = ((this.data[table]![id] as number | undefined) || 0) + by
|
|
64
|
+
this.data[table]![id] = newValue
|
|
65
|
+
return [id, newValue]
|
|
66
|
+
})
|
|
83
67
|
}
|
|
84
68
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Integer, KeyValueTuple, UnixTimestampNumber } from '@naturalcycles/js-lib'
|
|
2
2
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib'
|
|
3
3
|
import { CommonDBCreateOptions } from '../db.model'
|
|
4
4
|
|
|
@@ -29,48 +29,41 @@ export interface CommonKeyValueDB {
|
|
|
29
29
|
*
|
|
30
30
|
* Currently it is NOT required to maintain the same order as input `ids`.
|
|
31
31
|
*/
|
|
32
|
-
getByIds: (table: string, ids: string[]) => Promise<
|
|
32
|
+
getByIds: <V>(table: string, ids: string[]) => Promise<KeyValueTuple<string, V>[]>
|
|
33
33
|
|
|
34
34
|
deleteByIds: (table: string, ids: string[]) => Promise<void>
|
|
35
35
|
|
|
36
|
-
saveBatch: (
|
|
36
|
+
saveBatch: <V>(
|
|
37
37
|
table: string,
|
|
38
|
-
entries:
|
|
38
|
+
entries: KeyValueTuple<string, V>[],
|
|
39
39
|
opt?: CommonKeyValueDBSaveBatchOptions,
|
|
40
40
|
) => Promise<void>
|
|
41
41
|
|
|
42
42
|
streamIds: (table: string, limit?: number) => ReadableTyped<string>
|
|
43
|
-
streamValues: (table: string, limit?: number) => ReadableTyped<
|
|
44
|
-
streamEntries: (table: string, limit?: number) => ReadableTyped<
|
|
43
|
+
streamValues: <V>(table: string, limit?: number) => ReadableTyped<V>
|
|
44
|
+
streamEntries: <V>(table: string, limit?: number) => ReadableTyped<KeyValueTuple<string, V>>
|
|
45
45
|
|
|
46
46
|
count: (table: string) => Promise<number>
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
* Increments the value of a key in a table by a given amount.
|
|
50
|
-
* Default increment is 1 when `by` is not provided.
|
|
51
|
-
*
|
|
52
|
-
* Returns the new value.
|
|
53
|
-
*
|
|
54
|
-
* @experimental
|
|
55
|
-
*/
|
|
56
|
-
increment: (table: string, id: string, by?: number) => Promise<number>
|
|
57
|
-
|
|
58
48
|
/**
|
|
59
49
|
* Perform a batch of Increment operations.
|
|
60
|
-
* Given
|
|
50
|
+
* Given entries array, increment each key of it (1st index of the tuple) by the given amount (2nd index of the tuple).
|
|
61
51
|
*
|
|
62
52
|
* Example:
|
|
63
|
-
*
|
|
53
|
+
* [
|
|
54
|
+
* ['key1', 2],
|
|
55
|
+
* ['key2', 3],
|
|
56
|
+
* ]
|
|
64
57
|
* would increment `key1` by 2, and `key2` by 3.
|
|
65
58
|
*
|
|
66
|
-
* Returns the
|
|
59
|
+
* Returns the entries array with tuples of the same structure, with updated numbers.
|
|
67
60
|
*
|
|
68
61
|
* @experimental
|
|
69
62
|
*/
|
|
70
|
-
incrementBatch: (table: string,
|
|
63
|
+
incrementBatch: (table: string, entries: IncrementTuple[]) => Promise<IncrementTuple[]>
|
|
71
64
|
}
|
|
72
65
|
|
|
73
|
-
export type
|
|
66
|
+
export type IncrementTuple = [key: string, value: Integer]
|
|
74
67
|
|
|
75
68
|
export interface CommonKeyValueDBSaveBatchOptions {
|
|
76
69
|
/**
|
|
@@ -5,10 +5,10 @@ import { CommonDBCreateOptions } from '../db.model'
|
|
|
5
5
|
import {
|
|
6
6
|
CommonKeyValueDB,
|
|
7
7
|
CommonKeyValueDBSaveBatchOptions,
|
|
8
|
-
|
|
8
|
+
IncrementTuple,
|
|
9
9
|
} from './commonKeyValueDB'
|
|
10
10
|
|
|
11
|
-
export interface CommonKeyValueDaoCfg<V> {
|
|
11
|
+
export interface CommonKeyValueDaoCfg<RAW_V, V = RAW_V> {
|
|
12
12
|
db: CommonKeyValueDB
|
|
13
13
|
|
|
14
14
|
table: string
|
|
@@ -34,44 +34,33 @@ export interface CommonKeyValueDaoCfg<V> {
|
|
|
34
34
|
*/
|
|
35
35
|
logStarted?: boolean
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
mapValueToBuffer?: (v: V) => Promise<Buffer>
|
|
39
|
-
mapBufferToValue?: (b: Buffer) => Promise<V>
|
|
40
|
-
beforeCreate?: (v: Partial<V>) => Partial<V>
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Set to `true` to conveniently enable zipping+JSON.stringify
|
|
45
|
-
* (and unzipping+JSON.parse) of the Buffer value via hooks.
|
|
46
|
-
* Custom hooks will override these hooks (if provided).
|
|
47
|
-
*/
|
|
48
|
-
deflatedJsonValue?: boolean
|
|
37
|
+
transformer?: CommonKeyValueDaoTransformer<V, RAW_V>
|
|
49
38
|
}
|
|
50
39
|
|
|
51
40
|
export type CommonKeyValueDaoSaveOptions = CommonKeyValueDBSaveBatchOptions
|
|
52
41
|
|
|
42
|
+
export interface CommonKeyValueDaoTransformer<V, RAW_V> {
|
|
43
|
+
valueToRaw: (v: V) => Promise<RAW_V>
|
|
44
|
+
rawToValue: (raw: RAW_V) => Promise<V>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const commonKeyValueDaoDeflatedJsonTransformer: CommonKeyValueDaoTransformer<any, Buffer> = {
|
|
48
|
+
valueToRaw: async v => await deflateString(JSON.stringify(v)),
|
|
49
|
+
rawToValue: async raw => JSON.parse(await inflateToString(raw)),
|
|
50
|
+
}
|
|
51
|
+
|
|
53
52
|
// todo: logging
|
|
54
53
|
// todo: readonly
|
|
55
54
|
|
|
56
|
-
export class CommonKeyValueDao<
|
|
57
|
-
constructor(cfg: CommonKeyValueDaoCfg<V>) {
|
|
55
|
+
export class CommonKeyValueDao<K extends string, RAW_V, V = RAW_V> {
|
|
56
|
+
constructor(cfg: CommonKeyValueDaoCfg<RAW_V, V>) {
|
|
58
57
|
this.cfg = {
|
|
59
|
-
hooks: {},
|
|
60
58
|
logger: console,
|
|
61
59
|
...cfg,
|
|
62
60
|
}
|
|
63
|
-
|
|
64
|
-
if (cfg.deflatedJsonValue) {
|
|
65
|
-
this.cfg.hooks = {
|
|
66
|
-
mapValueToBuffer: async v => await deflateString(JSON.stringify(v)),
|
|
67
|
-
mapBufferToValue: async buf => JSON.parse(await inflateToString(buf)),
|
|
68
|
-
...cfg.hooks,
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
61
|
}
|
|
72
62
|
|
|
73
|
-
cfg: CommonKeyValueDaoCfg<V> & {
|
|
74
|
-
hooks: NonNullable<CommonKeyValueDaoCfg<V>['hooks']>
|
|
63
|
+
cfg: CommonKeyValueDaoCfg<RAW_V, V> & {
|
|
75
64
|
logger: CommonLogger
|
|
76
65
|
}
|
|
77
66
|
|
|
@@ -83,21 +72,15 @@ export class CommonKeyValueDao<V, K extends string = string> {
|
|
|
83
72
|
await this.cfg.db.createTable(this.cfg.table, opt)
|
|
84
73
|
}
|
|
85
74
|
|
|
86
|
-
create(input: Partial<V> = {}): V {
|
|
87
|
-
return {
|
|
88
|
-
...this.cfg.hooks.beforeCreate?.(input),
|
|
89
|
-
} as V
|
|
90
|
-
}
|
|
91
|
-
|
|
92
75
|
async getById(id?: K): Promise<V | null> {
|
|
93
76
|
if (!id) return null
|
|
94
77
|
const [r] = await this.getByIds([id])
|
|
95
78
|
return r?.[1] || null
|
|
96
79
|
}
|
|
97
80
|
|
|
98
|
-
async
|
|
81
|
+
async getByIdRaw(id?: K): Promise<RAW_V | null> {
|
|
99
82
|
if (!id) return null
|
|
100
|
-
const [r] = await this.cfg.db.getByIds(this.cfg.table, [id])
|
|
83
|
+
const [r] = await this.cfg.db.getByIds<RAW_V>(this.cfg.table, [id])
|
|
101
84
|
return r?.[1] || null
|
|
102
85
|
}
|
|
103
86
|
|
|
@@ -115,8 +98,8 @@ export class CommonKeyValueDao<V, K extends string = string> {
|
|
|
115
98
|
return r[1]
|
|
116
99
|
}
|
|
117
100
|
|
|
118
|
-
async
|
|
119
|
-
const [r] = await this.cfg.db.getByIds(this.cfg.table, [id])
|
|
101
|
+
async requireByIdRaw(id: K): Promise<RAW_V> {
|
|
102
|
+
const [r] = await this.cfg.db.getByIds<RAW_V>(this.cfg.table, [id])
|
|
120
103
|
|
|
121
104
|
if (!r) {
|
|
122
105
|
const { table } = this.cfg
|
|
@@ -129,46 +112,25 @@ export class CommonKeyValueDao<V, K extends string = string> {
|
|
|
129
112
|
return r[1]
|
|
130
113
|
}
|
|
131
114
|
|
|
132
|
-
async getByIdOrEmpty(id: K, part: Partial<V> = {}): Promise<V> {
|
|
133
|
-
const [r] = await this.getByIds([id])
|
|
134
|
-
if (r) return r[1]
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
...this.cfg.hooks.beforeCreate?.({}),
|
|
138
|
-
...part,
|
|
139
|
-
} as V
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async patch(id: K, patch: Partial<V>, opt?: CommonKeyValueDaoSaveOptions): Promise<V> {
|
|
143
|
-
const v: V = {
|
|
144
|
-
...(await this.getByIdOrEmpty(id)),
|
|
145
|
-
...patch,
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
await this.save(id, v, opt)
|
|
149
|
-
|
|
150
|
-
return v
|
|
151
|
-
}
|
|
152
|
-
|
|
153
115
|
async getByIds(ids: K[]): Promise<KeyValueTuple<string, V>[]> {
|
|
154
|
-
const entries = await this.cfg.db.getByIds(this.cfg.table, ids)
|
|
155
|
-
if (!this.cfg.
|
|
116
|
+
const entries = await this.cfg.db.getByIds<RAW_V>(this.cfg.table, ids)
|
|
117
|
+
if (!this.cfg.transformer) return entries as any
|
|
156
118
|
|
|
157
|
-
return await pMap(entries, async ([id,
|
|
119
|
+
return await pMap(entries, async ([id, raw]) => [
|
|
158
120
|
id,
|
|
159
|
-
await this.cfg.
|
|
121
|
+
await this.cfg.transformer!.rawToValue(raw),
|
|
160
122
|
])
|
|
161
123
|
}
|
|
162
124
|
|
|
163
|
-
async
|
|
164
|
-
return (await this.cfg.db.getByIds(this.cfg.table, ids)) as KeyValueTuple<K,
|
|
125
|
+
async getByIdsRaw(ids: K[]): Promise<KeyValueTuple<K, RAW_V>[]> {
|
|
126
|
+
return (await this.cfg.db.getByIds(this.cfg.table, ids)) as KeyValueTuple<K, RAW_V>[]
|
|
165
127
|
}
|
|
166
128
|
|
|
167
129
|
async save(id: K, value: V, opt?: CommonKeyValueDaoSaveOptions): Promise<void> {
|
|
168
130
|
await this.saveBatch([[id, value]], opt)
|
|
169
131
|
}
|
|
170
132
|
|
|
171
|
-
async
|
|
133
|
+
async saveRaw(id: K, value: RAW_V, opt?: CommonKeyValueDaoSaveOptions): Promise<void> {
|
|
172
134
|
await this.cfg.db.saveBatch(this.cfg.table, [[id, value]], opt)
|
|
173
135
|
}
|
|
174
136
|
|
|
@@ -176,20 +138,20 @@ export class CommonKeyValueDao<V, K extends string = string> {
|
|
|
176
138
|
entries: KeyValueTuple<K, V>[],
|
|
177
139
|
opt?: CommonKeyValueDaoSaveOptions,
|
|
178
140
|
): Promise<void> {
|
|
179
|
-
const {
|
|
180
|
-
let
|
|
141
|
+
const { transformer } = this.cfg
|
|
142
|
+
let rawEntries: KeyValueTuple<string, RAW_V>[]
|
|
181
143
|
|
|
182
|
-
if (!
|
|
183
|
-
|
|
144
|
+
if (!transformer) {
|
|
145
|
+
rawEntries = entries as any
|
|
184
146
|
} else {
|
|
185
|
-
|
|
147
|
+
rawEntries = await pMap(entries, async ([id, v]) => [id, await transformer.valueToRaw(v)])
|
|
186
148
|
}
|
|
187
149
|
|
|
188
|
-
await this.cfg.db.saveBatch(this.cfg.table,
|
|
150
|
+
await this.cfg.db.saveBatch(this.cfg.table, rawEntries, opt)
|
|
189
151
|
}
|
|
190
152
|
|
|
191
|
-
async
|
|
192
|
-
entries:
|
|
153
|
+
async saveBatchRaw(
|
|
154
|
+
entries: KeyValueTuple<K, RAW_V>[],
|
|
193
155
|
opt?: CommonKeyValueDaoSaveOptions,
|
|
194
156
|
): Promise<void> {
|
|
195
157
|
await this.cfg.db.saveBatch(this.cfg.table, entries, opt)
|
|
@@ -208,16 +170,16 @@ export class CommonKeyValueDao<V, K extends string = string> {
|
|
|
208
170
|
}
|
|
209
171
|
|
|
210
172
|
streamValues(limit?: number): ReadableTyped<V> {
|
|
211
|
-
const {
|
|
173
|
+
const { transformer } = this.cfg
|
|
212
174
|
|
|
213
|
-
if (!
|
|
214
|
-
return this.cfg.db.streamValues(this.cfg.table, limit)
|
|
175
|
+
if (!transformer) {
|
|
176
|
+
return this.cfg.db.streamValues<V>(this.cfg.table, limit)
|
|
215
177
|
}
|
|
216
178
|
|
|
217
|
-
return this.cfg.db.streamValues(this.cfg.table, limit).flatMap(
|
|
218
|
-
async
|
|
179
|
+
return this.cfg.db.streamValues<RAW_V>(this.cfg.table, limit).flatMap(
|
|
180
|
+
async raw => {
|
|
219
181
|
try {
|
|
220
|
-
return [await
|
|
182
|
+
return [await transformer.rawToValue(raw)]
|
|
221
183
|
} catch (err) {
|
|
222
184
|
this.cfg.logger.error(err)
|
|
223
185
|
return [] // SKIP
|
|
@@ -230,18 +192,18 @@ export class CommonKeyValueDao<V, K extends string = string> {
|
|
|
230
192
|
}
|
|
231
193
|
|
|
232
194
|
streamEntries(limit?: number): ReadableTyped<KeyValueTuple<K, V>> {
|
|
233
|
-
const {
|
|
195
|
+
const { transformer } = this.cfg
|
|
234
196
|
|
|
235
|
-
if (!
|
|
236
|
-
return this.cfg.db.streamEntries(this.cfg.table, limit) as
|
|
197
|
+
if (!transformer) {
|
|
198
|
+
return this.cfg.db.streamEntries<V>(this.cfg.table, limit) as any
|
|
237
199
|
}
|
|
238
200
|
|
|
239
201
|
return (
|
|
240
|
-
this.cfg.db.streamEntries(this.cfg.table, limit) as ReadableTyped<KeyValueTuple<K,
|
|
202
|
+
this.cfg.db.streamEntries(this.cfg.table, limit) as ReadableTyped<KeyValueTuple<K, RAW_V>>
|
|
241
203
|
).flatMap(
|
|
242
|
-
async ([id,
|
|
204
|
+
async ([id, raw]) => {
|
|
243
205
|
try {
|
|
244
|
-
return [[id, await
|
|
206
|
+
return [[id, await transformer.rawToValue(raw)]]
|
|
245
207
|
} catch (err) {
|
|
246
208
|
this.cfg.logger.error(err)
|
|
247
209
|
return [] // SKIP
|
|
@@ -272,6 +234,11 @@ export class CommonKeyValueDao<V, K extends string = string> {
|
|
|
272
234
|
* Returns the new value of the field.
|
|
273
235
|
*/
|
|
274
236
|
async increment(id: K, by = 1): Promise<number> {
|
|
275
|
-
|
|
237
|
+
const [t] = await this.cfg.db.incrementBatch(this.cfg.table, [[id, by]])
|
|
238
|
+
return t![1]
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async incrementBatch(entries: IncrementTuple[]): Promise<IncrementTuple[]> {
|
|
242
|
+
return await this.cfg.db.incrementBatch(this.cfg.table, entries)
|
|
276
243
|
}
|
|
277
244
|
}
|
|
@@ -2,7 +2,7 @@ import { AsyncMemoCache, localTime, MISS, NumberOfSeconds } from '@naturalcycles
|
|
|
2
2
|
import { CommonKeyValueDao } from './commonKeyValueDao'
|
|
3
3
|
|
|
4
4
|
export interface CommonKeyValueDaoMemoCacheCfg<VALUE> {
|
|
5
|
-
dao: CommonKeyValueDao<VALUE>
|
|
5
|
+
dao: CommonKeyValueDao<string, VALUE>
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* If set, every `set()` will set `expireAt` (TTL) option.
|
|
@@ -18,7 +18,7 @@ export interface CommonKeyValueDaoMemoCacheCfg<VALUE> {
|
|
|
18
18
|
* Also, does not support .clear(), as it's more dangerous than useful to actually
|
|
19
19
|
* clear the whole table/cache.
|
|
20
20
|
*/
|
|
21
|
-
export class CommonKeyValueDaoMemoCache<VALUE
|
|
21
|
+
export class CommonKeyValueDaoMemoCache<VALUE> implements AsyncMemoCache<string, VALUE> {
|
|
22
22
|
constructor(private cfg: CommonKeyValueDaoMemoCacheCfg<VALUE>) {}
|
|
23
23
|
|
|
24
24
|
async get(k: string): Promise<VALUE | typeof MISS> {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { _range, _sortBy } from '@naturalcycles/js-lib'
|
|
2
|
-
import { CommonKeyValueDB
|
|
1
|
+
import { _range, _sortBy, KeyValueTuple } from '@naturalcycles/js-lib'
|
|
2
|
+
import { CommonKeyValueDB } from '../kv/commonKeyValueDB'
|
|
3
3
|
import { TEST_TABLE } from './test.model'
|
|
4
4
|
|
|
5
5
|
const testIds = _range(1, 4).map(n => `id${n}`)
|
|
6
6
|
|
|
7
|
-
const testEntries:
|
|
7
|
+
const testEntries: KeyValueTuple<string, Buffer>[] = testIds.map(id => [
|
|
8
|
+
id,
|
|
9
|
+
Buffer.from(`${id}value`),
|
|
10
|
+
])
|
|
8
11
|
|
|
9
12
|
export function runCommonKeyValueDBTest(db: CommonKeyValueDB): void {
|
|
10
13
|
beforeAll(async () => {
|
|
@@ -47,7 +50,7 @@ export function runCommonKeyValueDBTest(db: CommonKeyValueDB): void {
|
|
|
47
50
|
test('saveBatch, then getByIds', async () => {
|
|
48
51
|
await db.saveBatch(TEST_TABLE, testEntries)
|
|
49
52
|
|
|
50
|
-
const entries = await db.getByIds(TEST_TABLE, testIds)
|
|
53
|
+
const entries = await db.getByIds<Buffer>(TEST_TABLE, testIds)
|
|
51
54
|
_sortBy(entries, e => e[0], true)
|
|
52
55
|
expect(entries).toEqual(testEntries)
|
|
53
56
|
})
|
|
@@ -73,26 +76,26 @@ export function runCommonKeyValueDBTest(db: CommonKeyValueDB): void {
|
|
|
73
76
|
})
|
|
74
77
|
|
|
75
78
|
test('streamValues', async () => {
|
|
76
|
-
const values = await db.streamValues(TEST_TABLE).toArray()
|
|
79
|
+
const values = await db.streamValues<Buffer>(TEST_TABLE).toArray()
|
|
77
80
|
values.sort()
|
|
78
81
|
expect(values).toEqual(testEntries.map(e => e[1]))
|
|
79
82
|
})
|
|
80
83
|
|
|
81
84
|
test('streamValues limited', async () => {
|
|
82
|
-
const valuesLimited = await db.streamValues(TEST_TABLE, 2).toArray()
|
|
85
|
+
const valuesLimited = await db.streamValues<Buffer>(TEST_TABLE, 2).toArray()
|
|
83
86
|
// valuesLimited.sort()
|
|
84
87
|
// expect(valuesLimited).toEqual(testEntries.map(e => e[1]).slice(0, 2))
|
|
85
88
|
expect(valuesLimited.length).toBe(2)
|
|
86
89
|
})
|
|
87
90
|
|
|
88
91
|
test('streamEntries', async () => {
|
|
89
|
-
const entries = await db.streamEntries(TEST_TABLE).toArray()
|
|
92
|
+
const entries = await db.streamEntries<Buffer>(TEST_TABLE).toArray()
|
|
90
93
|
entries.sort()
|
|
91
94
|
expect(entries).toEqual(testEntries)
|
|
92
95
|
})
|
|
93
96
|
|
|
94
97
|
test('streamEntries limited', async () => {
|
|
95
|
-
const entriesLimited = await db.streamEntries(TEST_TABLE, 2).toArray()
|
|
98
|
+
const entriesLimited = await db.streamEntries<Buffer>(TEST_TABLE, 2).toArray()
|
|
96
99
|
// entriesLimited.sort()
|
|
97
100
|
// expect(entriesLimited).toEqual(testEntries.slice(0, 2))
|
|
98
101
|
expect(entriesLimited.length).toBe(2)
|
|
@@ -109,26 +112,26 @@ export function runCommonKeyValueDBTest(db: CommonKeyValueDB): void {
|
|
|
109
112
|
const id2 = 'nonExistingField2'
|
|
110
113
|
|
|
111
114
|
test('increment on a non-existing field should set the value to 1', async () => {
|
|
112
|
-
const result = await db.
|
|
113
|
-
expect(result).
|
|
115
|
+
const result = await db.incrementBatch(TEST_TABLE, [[id, 1]])
|
|
116
|
+
expect(result).toEqual([[id, 1]])
|
|
114
117
|
})
|
|
115
118
|
|
|
116
119
|
test('increment on a existing field should increase the value by one', async () => {
|
|
117
|
-
const result = await db.
|
|
118
|
-
expect(result).
|
|
120
|
+
const result = await db.incrementBatch(TEST_TABLE, [[id, 1]])
|
|
121
|
+
expect(result).toEqual([[id, 2]])
|
|
119
122
|
})
|
|
120
123
|
|
|
121
124
|
test('increment should increase the value by the specified amount', async () => {
|
|
122
|
-
const result = await db.
|
|
123
|
-
expect(result).
|
|
125
|
+
const result = await db.incrementBatch(TEST_TABLE, [[id, 2]])
|
|
126
|
+
expect(result).toEqual([[id, 4]])
|
|
124
127
|
})
|
|
125
128
|
|
|
126
129
|
test('increment 2 ids at the same time', async () => {
|
|
127
|
-
const result = await db.incrementBatch(TEST_TABLE,
|
|
128
|
-
[id
|
|
129
|
-
[id2
|
|
130
|
-
|
|
131
|
-
expect(result).toEqual({
|
|
130
|
+
const result = await db.incrementBatch(TEST_TABLE, [
|
|
131
|
+
[id, 1],
|
|
132
|
+
[id2, 2],
|
|
133
|
+
])
|
|
134
|
+
expect(Object.fromEntries(result)).toEqual({
|
|
132
135
|
[id]: 5,
|
|
133
136
|
[id2]: 2,
|
|
134
137
|
})
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _sortBy, KeyValueTuple } from '@naturalcycles/js-lib'
|
|
2
2
|
import { CommonKeyValueDao } from '../kv/commonKeyValueDao'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
3
|
+
import { CommonKeyValueDB } from '../kv/commonKeyValueDB'
|
|
4
|
+
import { createTestItemsBM, TEST_TABLE, TestItemBM } from './test.model'
|
|
5
|
+
|
|
6
|
+
const testItems = createTestItemsBM(4)
|
|
7
|
+
const testIds = testItems.map(e => e.id)
|
|
8
|
+
const testEntries: KeyValueTuple<string, TestItemBM>[] = testItems.map(e => [e.id, e])
|
|
9
|
+
|
|
10
|
+
export function runCommonKeyValueDaoTest(db: CommonKeyValueDB): void {
|
|
11
|
+
const dao = new CommonKeyValueDao<string, TestItemBM>({
|
|
12
|
+
db,
|
|
13
|
+
table: TEST_TABLE,
|
|
14
|
+
// todo: make this test support "deflatedJson" transformer
|
|
15
|
+
})
|
|
16
|
+
const { support } = db
|
|
7
17
|
|
|
8
|
-
export function runCommonKeyValueDaoTest(dao: CommonKeyValueDao<Buffer>): void {
|
|
9
18
|
beforeAll(async () => {
|
|
10
19
|
// Tests in this suite are not isolated,
|
|
11
20
|
// and failing tests can leave the DB in an unexpected state for other tests,
|
|
@@ -20,8 +29,6 @@ export function runCommonKeyValueDaoTest(dao: CommonKeyValueDao<Buffer>): void {
|
|
|
20
29
|
await dao.deleteByIds(ids)
|
|
21
30
|
})
|
|
22
31
|
|
|
23
|
-
const { support } = dao.cfg.db
|
|
24
|
-
|
|
25
32
|
test('ping', async () => {
|
|
26
33
|
await dao.ping()
|
|
27
34
|
})
|
|
@@ -43,8 +50,12 @@ export function runCommonKeyValueDaoTest(dao: CommonKeyValueDao<Buffer>): void {
|
|
|
43
50
|
await dao.saveBatch(testEntries)
|
|
44
51
|
|
|
45
52
|
const entries = await dao.getByIds(testIds)
|
|
53
|
+
// console.log(typeof entries[0]![1], entries[0]![1])
|
|
54
|
+
|
|
46
55
|
_sortBy(entries, e => e[0], true)
|
|
47
|
-
expect(entries).toEqual(testEntries)
|
|
56
|
+
expect(entries).toEqual(testEntries) // Jest doesn't allow to compare Buffers directly
|
|
57
|
+
// expect(entries.map(e => e[0])).toEqual(testEntries.map(e => e[0]))
|
|
58
|
+
// expect(entries.map(e => e[1].toString())).toEqual(testEntries.map(e => e[1].toString()))
|
|
48
59
|
})
|
|
49
60
|
|
|
50
61
|
test('streamIds', async () => {
|
|
@@ -94,19 +105,33 @@ export function runCommonKeyValueDaoTest(dao: CommonKeyValueDao<Buffer>): void {
|
|
|
94
105
|
})
|
|
95
106
|
|
|
96
107
|
if (support.increment) {
|
|
108
|
+
const id = 'nonExistingField'
|
|
109
|
+
const id2 = 'nonExistingField2'
|
|
110
|
+
|
|
97
111
|
test('increment on a non-existing field should set the value to 1', async () => {
|
|
98
|
-
const result = await dao.increment(
|
|
112
|
+
const result = await dao.increment(id)
|
|
99
113
|
expect(result).toBe(1)
|
|
100
114
|
})
|
|
101
115
|
|
|
102
116
|
test('increment on a existing field should increase the value by one', async () => {
|
|
103
|
-
const result = await dao.increment(
|
|
117
|
+
const result = await dao.increment(id)
|
|
104
118
|
expect(result).toBe(2)
|
|
105
119
|
})
|
|
106
120
|
|
|
107
121
|
test('increment should increase the value by the specified amount', async () => {
|
|
108
|
-
const result = await dao.increment(
|
|
122
|
+
const result = await dao.increment(id, 2)
|
|
109
123
|
expect(result).toBe(4)
|
|
110
124
|
})
|
|
125
|
+
|
|
126
|
+
test('increment 2 ids at the same time', async () => {
|
|
127
|
+
const result = await dao.incrementBatch([
|
|
128
|
+
[id, 1],
|
|
129
|
+
[id2, 2],
|
|
130
|
+
])
|
|
131
|
+
expect(Object.fromEntries(result)).toEqual({
|
|
132
|
+
[id]: 5,
|
|
133
|
+
[id2]: 2,
|
|
134
|
+
})
|
|
135
|
+
})
|
|
111
136
|
}
|
|
112
137
|
}
|