@naturalcycles/db-lib 10.17.2 → 10.19.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.
@@ -1,7 +1,7 @@
1
1
  import type { Transform } from 'node:stream';
2
2
  import type { JsonSchemaObject, JsonSchemaRootObject } from '@naturalcycles/js-lib/json-schema';
3
3
  import type { CommonLogger } from '@naturalcycles/js-lib/log';
4
- import type { AsyncIndexedMapper, BaseDBEntity, StringMap, UnixTimestampMillis, Unsaved } from '@naturalcycles/js-lib/types';
4
+ import { type AsyncIndexedMapper, type BaseDBEntity, type NonNegativeInteger, type StringMap, type UnixTimestampMillis, type Unsaved } from '@naturalcycles/js-lib/types';
5
5
  import { type ReadableTyped } from '@naturalcycles/nodejs-lib/stream';
6
6
  import type { CommonDBTransactionOptions, DBTransaction, RunQueryResult } from '../db.model.js';
7
7
  import type { DBQuery } from '../query/dbQuery.js';
@@ -14,7 +14,7 @@ import type { CommonDaoCfg, CommonDaoCreateOptions, CommonDaoOptions, CommonDaoP
14
14
  * BM = Backend model (optimized for API access)
15
15
  * TM = Transport model (optimized to be sent over the wire)
16
16
  */
17
- export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM, ID = BM['id']> {
17
+ export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM, ID extends string = BM['id']> {
18
18
  cfg: CommonDaoCfg<BM, DBM, ID>;
19
19
  constructor(cfg: CommonDaoCfg<BM, DBM, ID>);
20
20
  create(part?: Partial<BM>, opt?: CommonDaoOptions): BM;
@@ -167,6 +167,26 @@ export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity
167
167
  * Proxy to this.cfg.db.ping
168
168
  */
169
169
  ping(): Promise<void>;
170
+ withId(id: ID): DaoWithId<typeof this>;
171
+ withIds(ids: ID[]): DaoWithIds<typeof this>;
172
+ toSave(input: BM | BM[]): DaoWithRows<typeof this>;
173
+ /**
174
+ * Load rows (by their ids) from Multiple tables at once.
175
+ * An optimized way to load data, minimizing DB round-trips.
176
+ *
177
+ * @experimental.
178
+ */
179
+ static multiGet<MAP extends Record<string, DaoWithIds<AnyDao> | DaoWithId<AnyDao>>>(inputMap: MAP, opt?: CommonDaoReadOptions): Promise<{
180
+ [K in keyof MAP]: MAP[K] extends DaoWithIds<any> ? InferBM<MAP[K]['dao']>[] : InferBM<MAP[K]['dao']> | null;
181
+ }>;
182
+ private static prepareMultiGetIds;
183
+ private static multiGetMapByTableById;
184
+ private static prepareMultiGetOutput;
185
+ /**
186
+ * Very @experimental.
187
+ */
188
+ static multiDeleteByIds(inputs: DaoWithIds<any>[], _opt?: CommonDaoOptions): Promise<NonNegativeInteger>;
189
+ static multiSave(inputs: DaoWithRows<any>[], opt?: CommonDaoSaveBatchOptions<any>): Promise<void>;
170
190
  createTransaction(opt?: CommonDBTransactionOptions): Promise<CommonDaoTransaction>;
171
191
  runInTransaction<T = void>(fn: CommonDaoTransactionFn<T>, opt?: CommonDBTransactionOptions): Promise<T>;
172
192
  /**
@@ -202,8 +222,8 @@ export declare class CommonDaoTransaction {
202
222
  * Never throws.
203
223
  */
204
224
  rollback(): Promise<void>;
205
- getById<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID = BM['id']>(dao: CommonDao<BM, DBM, ID>, id?: ID | null, opt?: CommonDaoReadOptions): Promise<BM | null>;
206
- getByIds<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID = BM['id']>(dao: CommonDao<BM, DBM, ID>, ids: ID[], opt?: CommonDaoReadOptions): Promise<BM[]>;
225
+ getById<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID extends string = BM['id']>(dao: CommonDao<BM, DBM, ID>, id?: ID | null, opt?: CommonDaoReadOptions): Promise<BM | null>;
226
+ getByIds<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID extends string = BM['id']>(dao: CommonDao<BM, DBM, ID>, ids: ID[], opt?: CommonDaoReadOptions): Promise<BM[]>;
207
227
  save<BM extends BaseDBEntity, DBM extends BaseDBEntity>(dao: CommonDao<BM, DBM>, bm: Unsaved<BM>, opt?: CommonDaoSaveOptions<BM, DBM>): Promise<BM>;
208
228
  saveBatch<BM extends BaseDBEntity, DBM extends BaseDBEntity>(dao: CommonDao<BM, DBM>, bms: Unsaved<BM>[], opt?: CommonDaoSaveBatchOptions<DBM>): Promise<BM[]>;
209
229
  /**
@@ -213,7 +233,23 @@ export declare class CommonDaoTransaction {
213
233
  *
214
234
  * So, this method is a rather simple convenience "Object.assign and then save".
215
235
  */
216
- patch<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID = BM['id']>(dao: CommonDao<BM, DBM, ID>, bm: BM, patch: Partial<BM>, opt?: CommonDaoSaveOptions<BM, DBM>): Promise<BM>;
217
- deleteById<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID = BM['id']>(dao: CommonDao<BM, DBM, ID>, id?: ID | null, opt?: CommonDaoOptions): Promise<number>;
218
- deleteByIds<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID = BM['id']>(dao: CommonDao<BM, DBM, ID>, ids: ID[], opt?: CommonDaoOptions): Promise<number>;
236
+ patch<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID extends string = BM['id']>(dao: CommonDao<BM, DBM, ID>, bm: BM, patch: Partial<BM>, opt?: CommonDaoSaveOptions<BM, DBM>): Promise<BM>;
237
+ deleteById<DAO extends AnyDao>(dao: DAO, id?: InferID<DAO> | null, opt?: CommonDaoOptions): Promise<number>;
238
+ deleteByIds<DAO extends AnyDao>(dao: DAO, ids: InferID<DAO>[], opt?: CommonDaoOptions): Promise<number>;
239
+ }
240
+ export interface DaoWithIds<DAO extends AnyDao> {
241
+ dao: DAO;
242
+ ids: string[];
243
+ }
244
+ export interface DaoWithId<DAO extends AnyDao> {
245
+ dao: DAO;
246
+ id: string;
247
+ }
248
+ export interface DaoWithRows<DAO extends AnyDao> {
249
+ dao: DAO;
250
+ rows: InferBM<DAO>[];
219
251
  }
252
+ type InferBM<DAO> = DAO extends CommonDao<infer BM> ? BM : never;
253
+ type InferID<DAO> = DAO extends CommonDao<any, any, infer ID> ? ID : never;
254
+ export type AnyDao = CommonDao<any>;
255
+ export {};
@@ -7,6 +7,7 @@ import { _deepJsonEquals } from '@naturalcycles/js-lib/object/deepEquals.js';
7
7
  import { _deepCopy, _filterUndefinedValues, _objectAssignExact, } from '@naturalcycles/js-lib/object/object.util.js';
8
8
  import { pMap } from '@naturalcycles/js-lib/promise/pMap.js';
9
9
  import { _truncate } from '@naturalcycles/js-lib/string/string.util.js';
10
+ import { _stringMapEntries, _stringMapValues, } from '@naturalcycles/js-lib/types';
10
11
  import { _passthroughPredicate, _typeCast, SKIP } from '@naturalcycles/js-lib/types';
11
12
  import { stringId } from '@naturalcycles/nodejs-lib';
12
13
  import { transformFlatten } from '@naturalcycles/nodejs-lib/stream';
@@ -981,6 +982,122 @@ export class CommonDao {
981
982
  async ping() {
982
983
  await this.cfg.db.ping();
983
984
  }
985
+ withId(id) {
986
+ return {
987
+ dao: this,
988
+ id,
989
+ };
990
+ }
991
+ withIds(ids) {
992
+ return {
993
+ dao: this,
994
+ ids,
995
+ };
996
+ }
997
+ toSave(input) {
998
+ return {
999
+ dao: this,
1000
+ rows: [input].flat(),
1001
+ };
1002
+ }
1003
+ /**
1004
+ * Load rows (by their ids) from Multiple tables at once.
1005
+ * An optimized way to load data, minimizing DB round-trips.
1006
+ *
1007
+ * @experimental.
1008
+ */
1009
+ static async multiGet(inputMap, opt = {}) {
1010
+ const db = Object.values(inputMap)[0]?.dao.cfg.db;
1011
+ if (!db) {
1012
+ return {};
1013
+ }
1014
+ const idsByTable = CommonDao.prepareMultiGetIds(inputMap);
1015
+ // todo: support tx
1016
+ const dbmsByTable = await db.multiGet(idsByTable, opt);
1017
+ const dbmByTableById = CommonDao.multiGetMapByTableById(dbmsByTable);
1018
+ return (await CommonDao.prepareMultiGetOutput(inputMap, dbmByTableById, opt));
1019
+ }
1020
+ static prepareMultiGetIds(inputMap) {
1021
+ const idSetByTable = {};
1022
+ for (const input of _stringMapValues(inputMap)) {
1023
+ const { table } = input.dao.cfg;
1024
+ idSetByTable[table] ||= new Set();
1025
+ if ('id' in input) {
1026
+ // Singular
1027
+ idSetByTable[table].add(input.id);
1028
+ }
1029
+ else {
1030
+ // Plural
1031
+ for (const id of input.ids) {
1032
+ idSetByTable[table].add(id);
1033
+ }
1034
+ }
1035
+ }
1036
+ const idsByTable = {};
1037
+ for (const [table, idSet] of _stringMapEntries(idSetByTable)) {
1038
+ idsByTable[table] = [...idSet];
1039
+ }
1040
+ return idsByTable;
1041
+ }
1042
+ static multiGetMapByTableById(dbmsByTable) {
1043
+ // We create this "map of maps", to be able to track the results back to the input props
1044
+ // This is needed to support:
1045
+ // - having multiple props from the same table
1046
+ const dbmByTableById = {};
1047
+ for (const [table, dbms] of _stringMapEntries(dbmsByTable)) {
1048
+ dbmByTableById[table] ||= {};
1049
+ for (const dbm of dbms) {
1050
+ dbmByTableById[table][dbm.id] = dbm;
1051
+ }
1052
+ }
1053
+ return dbmByTableById;
1054
+ }
1055
+ static async prepareMultiGetOutput(inputMap, dbmByTableById, opt = {}) {
1056
+ const bmsByProp = {};
1057
+ // Loop over input props again, to produce the output of the same shape as requested
1058
+ await pMap(_stringMapEntries(inputMap), async ([prop, input]) => {
1059
+ const { dao } = input;
1060
+ const { table } = dao.cfg;
1061
+ if ('id' in input) {
1062
+ // Singular
1063
+ const dbm = dbmByTableById[table][input.id];
1064
+ bmsByProp[prop] = (await dao.dbmToBM(dbm, opt)) || null;
1065
+ }
1066
+ else {
1067
+ // Plural
1068
+ // We apply filtering, to be able to support multiple input props fetching from the same table.
1069
+ // Without filtering - every prop will get ALL rows from that table.
1070
+ const dbms = input.ids.map(id => dbmByTableById[table][id]).filter(_isTruthy);
1071
+ bmsByProp[prop] = await dao.dbmsToBM(dbms, opt);
1072
+ }
1073
+ });
1074
+ return bmsByProp;
1075
+ }
1076
+ /**
1077
+ * Very @experimental.
1078
+ */
1079
+ static async multiDeleteByIds(inputs, _opt = {}) {
1080
+ if (!inputs.length)
1081
+ return 0;
1082
+ const { db } = inputs[0].dao.cfg;
1083
+ const idsByTable = {};
1084
+ for (const { dao, ids } of inputs) {
1085
+ idsByTable[dao.cfg.table] = ids;
1086
+ }
1087
+ return await db.multiDelete(idsByTable);
1088
+ }
1089
+ static async multiSave(inputs, opt = {}) {
1090
+ if (!inputs.length)
1091
+ return;
1092
+ const { db } = inputs[0].dao.cfg;
1093
+ const dbmsByTable = {};
1094
+ await pMap(inputs, async ({ dao, rows }) => {
1095
+ const { table } = dao.cfg;
1096
+ rows.forEach(bm => dao.assignIdCreatedUpdated(bm, opt));
1097
+ dbmsByTable[table] = await dao.bmsToDBM(rows, opt);
1098
+ });
1099
+ await db.multiSave(dbmsByTable);
1100
+ }
984
1101
  async createTransaction(opt) {
985
1102
  const tx = await this.cfg.db.createTransaction(opt);
986
1103
  return new CommonDaoTransaction(tx, this.cfg.logger);
@@ -1134,6 +1251,7 @@ export class CommonDaoTransaction {
1134
1251
  Object.assign(bm, patch);
1135
1252
  return await dao.save(bm, { ...opt, skipIfEquals, tx: this.tx });
1136
1253
  }
1254
+ // todo: use AnyDao/Infer in other methods as well, if this works well
1137
1255
  async deleteById(dao, id, opt) {
1138
1256
  if (!id)
1139
1257
  return 0;
@@ -1,7 +1,7 @@
1
1
  import type { ValidationFunction } from '@naturalcycles/js-lib';
2
2
  import type { AppError, ErrorMode } from '@naturalcycles/js-lib/error';
3
3
  import type { CommonLogger } from '@naturalcycles/js-lib/log';
4
- import type { BaseDBEntity, NumberOfMilliseconds, Promisable, UnixTimestamp } from '@naturalcycles/js-lib/types';
4
+ import type { BaseDBEntity, Promisable, UnixTimestamp } from '@naturalcycles/js-lib/types';
5
5
  import type { TransformLogProgressOptions, TransformMapOptions } from '@naturalcycles/nodejs-lib/stream';
6
6
  import type { CommonDB } from '../commondb/common.db.js';
7
7
  import type { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions } from '../db.model.js';
@@ -332,8 +332,3 @@ export interface CommonDaoStreamOptions<IN> extends CommonDaoReadOptions, Transf
332
332
  chunkConcurrency?: number;
333
333
  }
334
334
  export type CommonDaoCreateOptions = CommonDBCreateOptions;
335
- export interface OnValidationTimeData {
336
- tookMillis: NumberOfMilliseconds;
337
- table: string;
338
- obj: any;
339
- }
@@ -1,7 +1,7 @@
1
1
  import type { JsonSchemaObject, JsonSchemaRootObject } from '@naturalcycles/js-lib/json-schema';
2
2
  import type { ObjectWithId, StringMap } from '@naturalcycles/js-lib/types';
3
3
  import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream';
4
- import type { CommonDBOptions, CommonDBSaveOptions, CommonDBTransactionOptions, DBTransaction, DBTransactionFn, RunQueryResult } from '../db.model.js';
4
+ import type { CommonDBOptions, CommonDBReadOptions, CommonDBSaveOptions, CommonDBTransactionOptions, DBTransaction, DBTransactionFn, RunQueryResult } from '../db.model.js';
5
5
  import type { DBQuery } from '../query/dbQuery.js';
6
6
  import type { CommonDB, CommonDBSupport } from './common.db.js';
7
7
  import { CommonDBType } from './common.db.js';
@@ -19,6 +19,7 @@ export declare class BaseCommonDB implements CommonDB {
19
19
  getByIds<ROW extends ObjectWithId>(_table: string, _ids: string[]): Promise<ROW[]>;
20
20
  deleteByQuery<ROW extends ObjectWithId>(_q: DBQuery<ROW>): Promise<number>;
21
21
  patchByQuery<ROW extends ObjectWithId>(_q: DBQuery<ROW>, _patch: Partial<ROW>, _opt?: CommonDBOptions): Promise<number>;
22
+ patchById<ROW extends ObjectWithId>(_table: string, _id: string, _patch: Partial<ROW>, _opt?: CommonDBOptions): Promise<void>;
22
23
  runQuery<ROW extends ObjectWithId>(_q: DBQuery<ROW>): Promise<RunQueryResult<ROW>>;
23
24
  runQueryCount<ROW extends ObjectWithId>(_q: DBQuery<ROW>): Promise<number>;
24
25
  saveBatch<ROW extends ObjectWithId>(_table: string, _rows: ROW[], _opt?: CommonDBSaveOptions<ROW>): Promise<void>;
@@ -27,4 +28,7 @@ export declare class BaseCommonDB implements CommonDB {
27
28
  runInTransaction(fn: DBTransactionFn, _opt?: CommonDBTransactionOptions): Promise<void>;
28
29
  createTransaction(_opt?: CommonDBTransactionOptions): Promise<DBTransaction>;
29
30
  incrementBatch(_table: string, _prop: string, _incrementMap: StringMap<number>, _opt?: CommonDBOptions): Promise<StringMap<number>>;
31
+ multiGet<ROW extends ObjectWithId>(_map: StringMap<string[]>, _opt?: CommonDBReadOptions): Promise<StringMap<ROW[]>>;
32
+ multiSave<ROW extends ObjectWithId>(_map: StringMap<ROW[]>, _opt?: CommonDBSaveOptions<ROW>): Promise<void>;
33
+ multiDelete(_map: StringMap<string[]>, _opt?: CommonDBOptions): Promise<number>;
30
34
  }
@@ -28,6 +28,9 @@ export class BaseCommonDB {
28
28
  async patchByQuery(_q, _patch, _opt) {
29
29
  throw new Error('patchByQuery is not implemented');
30
30
  }
31
+ async patchById(_table, _id, _patch, _opt) {
32
+ throw new Error('patchById is not implemented');
33
+ }
31
34
  async runQuery(_q) {
32
35
  throw new Error('runQuery is not implemented');
33
36
  }
@@ -54,4 +57,13 @@ export class BaseCommonDB {
54
57
  async incrementBatch(_table, _prop, _incrementMap, _opt) {
55
58
  throw new Error('incrementBatch is not implemented');
56
59
  }
60
+ async multiGet(_map, _opt) {
61
+ throw new Error('multiGetByIds is not implemented');
62
+ }
63
+ async multiSave(_map, _opt) {
64
+ throw new Error('multiSaveBatch is not implemented');
65
+ }
66
+ async multiDelete(_map, _opt) {
67
+ throw new Error('multiDeleteByIds is not implemented');
68
+ }
57
69
  }
@@ -1,12 +1,8 @@
1
1
  import type { JsonSchemaObject, JsonSchemaRootObject } from '@naturalcycles/js-lib/json-schema';
2
- import type { ObjectWithId, StringMap } from '@naturalcycles/js-lib/types';
2
+ import type { NonNegativeInteger, ObjectWithId, StringMap } from '@naturalcycles/js-lib/types';
3
3
  import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream';
4
4
  import type { CommonDBCreateOptions, CommonDBOptions, CommonDBReadOptions, CommonDBSaveOptions, CommonDBStreamOptions, CommonDBTransactionOptions, DBTransaction, DBTransactionFn, RunQueryResult } from '../db.model.js';
5
5
  import type { DBQuery } from '../query/dbQuery.js';
6
- export declare enum CommonDBType {
7
- 'document' = "document",
8
- 'relational' = "relational"
9
- }
10
6
  export interface CommonDB {
11
7
  /**
12
8
  * Relational databases are expected to return `null` for all missing properties.
@@ -44,26 +40,83 @@ export interface CommonDB {
44
40
  * (Such limitation exists because Datastore doesn't support it).
45
41
  */
46
42
  getByIds: <ROW extends ObjectWithId>(table: string, ids: string[], opt?: CommonDBReadOptions) => Promise<ROW[]>;
43
+ /**
44
+ * Get rows from multiple tables at once.
45
+ * Mimics the API of some NoSQL databases like Firestore.
46
+ *
47
+ * Takes `map`, which is a map from "table name" to an array of ids.
48
+ * Example:
49
+ * {
50
+ * 'TableOne': ['id1', 'id2'],
51
+ * 'TableTwo': ['id3'],
52
+ * }
53
+ *
54
+ * Returns a map with the same keys (table names) and arrays of rows as values.
55
+ * Even if some table is not found, it will return an empty array of results for that table.
56
+ *
57
+ * @experimental
58
+ */
59
+ multiGet: <ROW extends ObjectWithId>(idsByTable: StringMap<string[]>, opt?: CommonDBReadOptions) => Promise<StringMap<ROW[]>>;
47
60
  /**
48
61
  * Order by 'id' is not supported by all implementations (for example, Datastore doesn't support it).
49
62
  */
50
63
  runQuery: <ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBReadOptions) => Promise<RunQueryResult<ROW>>;
51
- runQueryCount: <ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBReadOptions) => Promise<number>;
64
+ runQueryCount: <ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBReadOptions) => Promise<NonNegativeInteger>;
52
65
  streamQuery: <ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBStreamOptions) => ReadableTyped<ROW>;
53
66
  /**
54
67
  * rows can have missing ids only if DB supports auto-generating them (like mysql auto_increment).
55
68
  */
56
69
  saveBatch: <ROW extends ObjectWithId>(table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>) => Promise<void>;
70
+ /**
71
+ * Save rows for multiple tables at once.
72
+ * Mimics the API of some NoSQL databases like Firestore.
73
+ *
74
+ * Takes `map`, which is a map from "table name" to an array of rows.
75
+ * Example:
76
+ * {
77
+ * 'TableOne': [{ id: 'id1', ... }, { id: 'id2', ... }],
78
+ * 'TableTwo': [{ id: 'id3', ... }],
79
+ * }
80
+ *
81
+ * @experimental
82
+ */
83
+ multiSave: <ROW extends ObjectWithId>(rowsByTable: StringMap<ROW[]>, opt?: CommonDBSaveOptions<ROW>) => Promise<void>;
84
+ /**
85
+ * Perform a partial update of a row by its id.
86
+ * Unlike save - doesn't require to first load the doc.
87
+ * Mimics the API of some NoSQL databases like Firestore.
88
+ *
89
+ * The object with given id has to exist, otherwise an error will be thrown.
90
+ *
91
+ * @experimental
92
+ */
93
+ patchById: <ROW extends ObjectWithId>(table: string, id: string, patch: Partial<ROW>, opt?: CommonDBOptions) => Promise<void>;
57
94
  /**
58
95
  * Returns number of deleted items.
59
96
  * Not supported by all implementations (e.g Datastore will always return same number as number of ids).
60
97
  */
61
- deleteByIds: (table: string, ids: string[], opt?: CommonDBOptions) => Promise<number>;
98
+ deleteByIds: (table: string, ids: string[], opt?: CommonDBOptions) => Promise<NonNegativeInteger>;
62
99
  /**
100
+ * Deletes rows from multiple tables at once.
101
+ * Mimics the API of some NoSQL databases like Firestore.
102
+ * Takes `map`, which is a map from "table name" to an array of ids to delete.
103
+ * Example:
104
+ * {
105
+ * 'TableOne': ['id1', 'id2'],
106
+ * 'TableTwo': ['id3'],
107
+ * }
108
+ *
63
109
  * Returns number of deleted items.
64
110
  * Not supported by all implementations (e.g Datastore will always return same number as number of ids).
111
+ *
112
+ * @experimental
65
113
  */
66
- deleteByQuery: <ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions) => Promise<number>;
114
+ multiDelete: (idsByTable: StringMap<string[]>, opt?: CommonDBOptions) => Promise<NonNegativeInteger>;
115
+ /**
116
+ * Returns number of deleted items.
117
+ * Not supported by all implementations (e.g Datastore will always return same number as number of ids).
118
+ */
119
+ deleteByQuery: <ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions) => Promise<NonNegativeInteger>;
67
120
  /**
68
121
  * Applies patch to all the rows that are matched by the query.
69
122
  *
@@ -111,6 +164,10 @@ export interface CommonDB {
111
164
  */
112
165
  incrementBatch: (table: string, prop: string, incrementMap: StringMap<number>, opt?: CommonDBOptions) => Promise<StringMap<number>>;
113
166
  }
167
+ export declare enum CommonDBType {
168
+ 'document' = "document",
169
+ 'relational' = "relational"
170
+ }
114
171
  /**
115
172
  * Manifest of supported features.
116
173
  */
@@ -123,6 +180,7 @@ export interface CommonDBSupport {
123
180
  insertSaveMethod?: boolean;
124
181
  updateSaveMethod?: boolean;
125
182
  patchByQuery?: boolean;
183
+ patchById?: boolean;
126
184
  increment?: boolean;
127
185
  createTable?: boolean;
128
186
  tableSchemas?: boolean;
@@ -131,5 +189,6 @@ export interface CommonDBSupport {
131
189
  nullValues?: boolean;
132
190
  transactions?: boolean;
133
191
  timeMachine?: boolean;
192
+ multiTableOperations?: boolean;
134
193
  }
135
- export declare const commonDBFullSupport: CommonDBSupport;
194
+ export declare const commonDBFullSupport: Required<CommonDBSupport>;
@@ -12,6 +12,7 @@ export const commonDBFullSupport = {
12
12
  insertSaveMethod: true,
13
13
  updateSaveMethod: true,
14
14
  patchByQuery: true,
15
+ patchById: true,
15
16
  increment: true,
16
17
  createTable: true,
17
18
  tableSchemas: true,
@@ -20,4 +21,5 @@ export const commonDBFullSupport = {
20
21
  nullValues: true,
21
22
  transactions: true,
22
23
  timeMachine: true,
24
+ multiTableOperations: true,
23
25
  };
@@ -50,9 +50,13 @@ export declare class InMemoryDB implements CommonDB {
50
50
  getTableSchema<ROW extends ObjectWithId>(_table: string): Promise<JsonSchemaRootObject<ROW>>;
51
51
  createTable<ROW extends ObjectWithId>(_table: string, _schema: JsonSchemaObject<ROW>, opt?: CommonDBCreateOptions): Promise<void>;
52
52
  getByIds<ROW extends ObjectWithId>(_table: string, ids: string[], _opt?: CommonDBOptions): Promise<ROW[]>;
53
+ multiGet<ROW extends ObjectWithId>(map: StringMap<string[]>, _opt?: CommonDBOptions): Promise<StringMap<ROW[]>>;
53
54
  saveBatch<ROW extends ObjectWithId>(_table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
55
+ multiSave<ROW extends ObjectWithId>(map: StringMap<ROW[]>, opt?: CommonDBSaveOptions<ROW>): Promise<void>;
56
+ patchById<ROW extends ObjectWithId>(_table: string, id: string, patch: Partial<ROW>, _opt?: CommonDBOptions): Promise<void>;
54
57
  deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<number>;
55
58
  deleteByIds(_table: string, ids: string[], _opt?: CommonDBOptions): Promise<number>;
59
+ multiDelete(map: StringMap<string[]>, _opt?: CommonDBOptions): Promise<number>;
56
60
  patchByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, patch: Partial<ROW>): Promise<number>;
57
61
  runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<RunQueryResult<ROW>>;
58
62
  runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<number>;
@@ -74,18 +74,28 @@ export class InMemoryDB {
74
74
  this.data[table] ||= {};
75
75
  return ids.map(id => this.data[table][id]).filter(Boolean);
76
76
  }
77
+ async multiGet(map, _opt = {}) {
78
+ const result = {};
79
+ for (const [tableName, ids] of _stringMapEntries(map)) {
80
+ const table = this.cfg.tablesPrefix + tableName;
81
+ result[table] = ids.map(id => this.data[table]?.[id]).filter(Boolean);
82
+ }
83
+ return result;
84
+ }
77
85
  async saveBatch(_table, rows, opt = {}) {
78
86
  const table = this.cfg.tablesPrefix + _table;
79
87
  this.data[table] ||= {};
88
+ const isInsert = opt.saveMethod === 'insert';
89
+ const isUpdate = opt.saveMethod === 'update';
80
90
  for (const r of rows) {
81
91
  if (!r.id) {
82
92
  this.cfg.logger.warn({ rows });
83
93
  throw new Error(`InMemoryDB doesn't support id auto-generation in saveBatch, row without id was given`);
84
94
  }
85
- if (opt.saveMethod === 'insert' && this.data[table][r.id]) {
95
+ if (isInsert && this.data[table][r.id]) {
86
96
  throw new Error(`InMemoryDB: INSERT failed, entity exists: ${table}.${r.id}`);
87
97
  }
88
- if (opt.saveMethod === 'update' && !this.data[table][r.id]) {
98
+ if (isUpdate && !this.data[table][r.id]) {
89
99
  throw new Error(`InMemoryDB: UPDATE failed, entity doesn't exist: ${table}.${r.id}`);
90
100
  }
91
101
  // JSON parse/stringify (deep clone) is to:
@@ -94,6 +104,16 @@ export class InMemoryDB {
94
104
  this.data[table][r.id] = JSON.parse(JSON.stringify(r), bufferReviver);
95
105
  }
96
106
  }
107
+ async multiSave(map, opt = {}) {
108
+ for (const [table, rows] of _stringMapEntries(map)) {
109
+ await this.saveBatch(table, rows, opt);
110
+ }
111
+ }
112
+ async patchById(_table, id, patch, _opt) {
113
+ const table = this.cfg.tablesPrefix + _table;
114
+ _assert(!this.data[table]?.[id], `InMemoryDB: patchById failed, entity doesn't exist: ${table}.${id}`);
115
+ Object.assign(this.data[table][id], patch);
116
+ }
97
117
  async deleteByQuery(q, _opt) {
98
118
  const table = this.cfg.tablesPrefix + q.table;
99
119
  if (!this.data[table])
@@ -114,6 +134,13 @@ export class InMemoryDB {
114
134
  }
115
135
  return count;
116
136
  }
137
+ async multiDelete(map, _opt) {
138
+ let count = 0;
139
+ for (const [table, ids] of _stringMapEntries(map)) {
140
+ count += await this.deleteByIds(table, ids, _opt);
141
+ }
142
+ return count;
143
+ }
117
144
  async patchByQuery(q, patch) {
118
145
  if (_isEmptyObject(patch))
119
146
  return 0;
@@ -87,7 +87,7 @@ export declare class DBQuery<ROW extends ObjectWithId> {
87
87
  /**
88
88
  * DBQuery that has additional method to support Fluent API style.
89
89
  */
90
- export declare class RunnableDBQuery<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM, ID = BM['id']> extends DBQuery<DBM> {
90
+ export declare class RunnableDBQuery<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM, ID extends string = BM['id']> extends DBQuery<DBM> {
91
91
  dao: CommonDao<BM, DBM, ID>;
92
92
  /**
93
93
  * Pass `table` to override table.
@@ -2,6 +2,7 @@ import type { JsonSchemaObject } from '@naturalcycles/js-lib/json-schema';
2
2
  import type { BaseDBEntity } from '@naturalcycles/js-lib/types';
3
3
  import { type ObjectSchema } from '@naturalcycles/nodejs-lib/joi';
4
4
  export declare const TEST_TABLE = "TEST_TABLE";
5
+ export declare const TEST_TABLE_2 = "TEST_TABLE_2";
5
6
  export interface TestItemBM extends BaseDBEntity {
6
7
  k1: string;
7
8
  k2?: string | null;
@@ -3,6 +3,7 @@ import { j } from '@naturalcycles/js-lib/json-schema';
3
3
  import { baseDBEntitySchema, binarySchema, booleanSchema, numberSchema, objectSchema, stringSchema, } from '@naturalcycles/nodejs-lib/joi';
4
4
  const MOCK_TS_2018_06_21 = 1529539200;
5
5
  export const TEST_TABLE = 'TEST_TABLE';
6
+ export const TEST_TABLE_2 = 'TEST_TABLE_2';
6
7
  export const testItemBMSchema = objectSchema({
7
8
  k1: stringSchema,
8
9
  k2: stringSchema.allow(null).optional(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/db-lib",
3
3
  "type": "module",
4
- "version": "10.17.2",
4
+ "version": "10.19.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@naturalcycles/nodejs-lib": "^15"
@@ -1,12 +1,7 @@
1
1
  import type { ValidationFunction } from '@naturalcycles/js-lib'
2
2
  import type { AppError, ErrorMode } from '@naturalcycles/js-lib/error'
3
3
  import type { CommonLogger } from '@naturalcycles/js-lib/log'
4
- import type {
5
- BaseDBEntity,
6
- NumberOfMilliseconds,
7
- Promisable,
8
- UnixTimestamp,
9
- } from '@naturalcycles/js-lib/types'
4
+ import type { BaseDBEntity, Promisable, UnixTimestamp } from '@naturalcycles/js-lib/types'
10
5
  import type {
11
6
  TransformLogProgressOptions,
12
7
  TransformMapOptions,
@@ -403,9 +398,3 @@ export interface CommonDaoStreamOptions<IN>
403
398
  }
404
399
 
405
400
  export type CommonDaoCreateOptions = CommonDBCreateOptions
406
-
407
- export interface OnValidationTimeData {
408
- tookMillis: NumberOfMilliseconds
409
- table: string
410
- obj: any
411
- }
@@ -14,13 +14,16 @@ import {
14
14
  } from '@naturalcycles/js-lib/object/object.util.js'
15
15
  import { pMap } from '@naturalcycles/js-lib/promise/pMap.js'
16
16
  import { _truncate } from '@naturalcycles/js-lib/string/string.util.js'
17
- import type {
18
- AsyncIndexedMapper,
19
- BaseDBEntity,
20
- ObjectWithId,
21
- StringMap,
22
- UnixTimestampMillis,
23
- Unsaved,
17
+ import {
18
+ _stringMapEntries,
19
+ _stringMapValues,
20
+ type AsyncIndexedMapper,
21
+ type BaseDBEntity,
22
+ type NonNegativeInteger,
23
+ type ObjectWithId,
24
+ type StringMap,
25
+ type UnixTimestampMillis,
26
+ type Unsaved,
24
27
  } from '@naturalcycles/js-lib/types'
25
28
  import { _passthroughPredicate, _typeCast, SKIP } from '@naturalcycles/js-lib/types'
26
29
  import { stringId } from '@naturalcycles/nodejs-lib'
@@ -61,7 +64,11 @@ import { CommonDaoLogLevel } from './common.dao.model.js'
61
64
  * BM = Backend model (optimized for API access)
62
65
  * TM = Transport model (optimized to be sent over the wire)
63
66
  */
64
- export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM, ID = BM['id']> {
67
+ export class CommonDao<
68
+ BM extends BaseDBEntity,
69
+ DBM extends BaseDBEntity = BM,
70
+ ID extends string = BM['id'],
71
+ > {
65
72
  constructor(public cfg: CommonDaoCfg<BM, DBM, ID>) {
66
73
  this.cfg = {
67
74
  logLevel: CommonDaoLogLevel.NONE,
@@ -1257,6 +1264,159 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM, I
1257
1264
  await this.cfg.db.ping()
1258
1265
  }
1259
1266
 
1267
+ withId(id: ID): DaoWithId<typeof this> {
1268
+ return {
1269
+ dao: this,
1270
+ id,
1271
+ }
1272
+ }
1273
+
1274
+ withIds(ids: ID[]): DaoWithIds<typeof this> {
1275
+ return {
1276
+ dao: this,
1277
+ ids,
1278
+ }
1279
+ }
1280
+
1281
+ toSave(input: BM | BM[]): DaoWithRows<typeof this> {
1282
+ return {
1283
+ dao: this,
1284
+ rows: [input].flat() as any[],
1285
+ }
1286
+ }
1287
+
1288
+ /**
1289
+ * Load rows (by their ids) from Multiple tables at once.
1290
+ * An optimized way to load data, minimizing DB round-trips.
1291
+ *
1292
+ * @experimental.
1293
+ */
1294
+ static async multiGet<MAP extends Record<string, DaoWithIds<AnyDao> | DaoWithId<AnyDao>>>(
1295
+ inputMap: MAP,
1296
+ opt: CommonDaoReadOptions = {},
1297
+ ): Promise<{
1298
+ [K in keyof MAP]: MAP[K] extends DaoWithIds<any>
1299
+ ? InferBM<MAP[K]['dao']>[]
1300
+ : InferBM<MAP[K]['dao']> | null
1301
+ }> {
1302
+ const db = Object.values(inputMap)[0]?.dao.cfg.db
1303
+ if (!db) {
1304
+ return {} as any
1305
+ }
1306
+
1307
+ const idsByTable = CommonDao.prepareMultiGetIds(inputMap)
1308
+
1309
+ // todo: support tx
1310
+ const dbmsByTable = await db.multiGet(idsByTable, opt)
1311
+
1312
+ const dbmByTableById = CommonDao.multiGetMapByTableById(dbmsByTable)
1313
+
1314
+ return (await CommonDao.prepareMultiGetOutput(inputMap, dbmByTableById, opt)) as any
1315
+ }
1316
+
1317
+ private static prepareMultiGetIds(
1318
+ inputMap: StringMap<DaoWithIds<AnyDao> | DaoWithId<AnyDao>>,
1319
+ ): StringMap<string[]> {
1320
+ const idSetByTable: StringMap<Set<string>> = {}
1321
+
1322
+ for (const input of _stringMapValues(inputMap)) {
1323
+ const { table } = input.dao.cfg
1324
+ idSetByTable[table] ||= new Set()
1325
+ if ('id' in input) {
1326
+ // Singular
1327
+ idSetByTable[table].add(input.id)
1328
+ } else {
1329
+ // Plural
1330
+ for (const id of input.ids) {
1331
+ idSetByTable[table].add(id)
1332
+ }
1333
+ }
1334
+ }
1335
+
1336
+ const idsByTable: StringMap<string[]> = {}
1337
+ for (const [table, idSet] of _stringMapEntries(idSetByTable)) {
1338
+ idsByTable[table] = [...idSet]
1339
+ }
1340
+ return idsByTable
1341
+ }
1342
+
1343
+ private static multiGetMapByTableById(
1344
+ dbmsByTable: StringMap<ObjectWithId[]>,
1345
+ ): StringMap<StringMap<ObjectWithId>> {
1346
+ // We create this "map of maps", to be able to track the results back to the input props
1347
+ // This is needed to support:
1348
+ // - having multiple props from the same table
1349
+ const dbmByTableById: StringMap<StringMap<ObjectWithId>> = {}
1350
+ for (const [table, dbms] of _stringMapEntries(dbmsByTable)) {
1351
+ dbmByTableById[table] ||= {}
1352
+ for (const dbm of dbms) {
1353
+ dbmByTableById[table][dbm.id] = dbm
1354
+ }
1355
+ }
1356
+
1357
+ return dbmByTableById
1358
+ }
1359
+
1360
+ private static async prepareMultiGetOutput(
1361
+ inputMap: StringMap<DaoWithIds<AnyDao> | DaoWithId<AnyDao>>,
1362
+ dbmByTableById: StringMap<StringMap<ObjectWithId>>,
1363
+ opt: CommonDaoReadOptions = {},
1364
+ ): Promise<StringMap<unknown>> {
1365
+ const bmsByProp: StringMap<unknown> = {}
1366
+
1367
+ // Loop over input props again, to produce the output of the same shape as requested
1368
+ await pMap(_stringMapEntries(inputMap), async ([prop, input]) => {
1369
+ const { dao } = input
1370
+ const { table } = dao.cfg
1371
+ if ('id' in input) {
1372
+ // Singular
1373
+ const dbm = dbmByTableById[table]![input.id]
1374
+ bmsByProp[prop] = (await dao.dbmToBM(dbm, opt)) || null
1375
+ } else {
1376
+ // Plural
1377
+ // We apply filtering, to be able to support multiple input props fetching from the same table.
1378
+ // Without filtering - every prop will get ALL rows from that table.
1379
+ const dbms = input.ids.map(id => dbmByTableById[table]![id]).filter(_isTruthy)
1380
+ bmsByProp[prop] = await dao.dbmsToBM(dbms, opt)
1381
+ }
1382
+ })
1383
+
1384
+ return bmsByProp as any
1385
+ }
1386
+
1387
+ /**
1388
+ * Very @experimental.
1389
+ */
1390
+ static async multiDeleteByIds(
1391
+ inputs: DaoWithIds<any>[],
1392
+ _opt: CommonDaoOptions = {},
1393
+ ): Promise<NonNegativeInteger> {
1394
+ if (!inputs.length) return 0
1395
+ const { db } = inputs[0]!.dao.cfg
1396
+ const idsByTable: StringMap<string[]> = {}
1397
+ for (const { dao, ids } of inputs) {
1398
+ idsByTable[dao.cfg.table] = ids
1399
+ }
1400
+
1401
+ return await db.multiDelete(idsByTable)
1402
+ }
1403
+
1404
+ static async multiSave(
1405
+ inputs: DaoWithRows<any>[],
1406
+ opt: CommonDaoSaveBatchOptions<any> = {},
1407
+ ): Promise<void> {
1408
+ if (!inputs.length) return
1409
+ const { db } = inputs[0]!.dao.cfg
1410
+ const dbmsByTable: StringMap<any[]> = {}
1411
+ await pMap(inputs, async ({ dao, rows }) => {
1412
+ const { table } = dao.cfg
1413
+ rows.forEach(bm => dao.assignIdCreatedUpdated(bm, opt))
1414
+ dbmsByTable[table] = await dao.bmsToDBM(rows, opt)
1415
+ })
1416
+
1417
+ await db.multiSave(dbmsByTable)
1418
+ }
1419
+
1260
1420
  async createTransaction(opt?: CommonDBTransactionOptions): Promise<CommonDaoTransaction> {
1261
1421
  const tx = await this.cfg.db.createTransaction(opt)
1262
1422
  return new CommonDaoTransaction(tx, this.cfg.logger!)
@@ -1395,7 +1555,7 @@ export class CommonDaoTransaction {
1395
1555
  }
1396
1556
  }
1397
1557
 
1398
- async getById<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID = BM['id']>(
1558
+ async getById<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID extends string = BM['id']>(
1399
1559
  dao: CommonDao<BM, DBM, ID>,
1400
1560
  id?: ID | null,
1401
1561
  opt?: CommonDaoReadOptions,
@@ -1403,7 +1563,7 @@ export class CommonDaoTransaction {
1403
1563
  return await dao.getById(id, { ...opt, tx: this.tx })
1404
1564
  }
1405
1565
 
1406
- async getByIds<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID = BM['id']>(
1566
+ async getByIds<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID extends string = BM['id']>(
1407
1567
  dao: CommonDao<BM, DBM, ID>,
1408
1568
  ids: ID[],
1409
1569
  opt?: CommonDaoReadOptions,
@@ -1448,7 +1608,7 @@ export class CommonDaoTransaction {
1448
1608
  *
1449
1609
  * So, this method is a rather simple convenience "Object.assign and then save".
1450
1610
  */
1451
- async patch<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID = BM['id']>(
1611
+ async patch<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID extends string = BM['id']>(
1452
1612
  dao: CommonDao<BM, DBM, ID>,
1453
1613
  bm: BM,
1454
1614
  patch: Partial<BM>,
@@ -1459,20 +1619,42 @@ export class CommonDaoTransaction {
1459
1619
  return await dao.save(bm, { ...opt, skipIfEquals, tx: this.tx })
1460
1620
  }
1461
1621
 
1462
- async deleteById<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID = BM['id']>(
1463
- dao: CommonDao<BM, DBM, ID>,
1464
- id?: ID | null,
1622
+ // todo: use AnyDao/Infer in other methods as well, if this works well
1623
+ async deleteById<DAO extends AnyDao>(
1624
+ dao: DAO,
1625
+ id?: InferID<DAO> | null,
1465
1626
  opt?: CommonDaoOptions,
1466
1627
  ): Promise<number> {
1467
1628
  if (!id) return 0
1468
1629
  return await this.deleteByIds(dao, [id], opt)
1469
1630
  }
1470
1631
 
1471
- async deleteByIds<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID = BM['id']>(
1472
- dao: CommonDao<BM, DBM, ID>,
1473
- ids: ID[],
1632
+ async deleteByIds<DAO extends AnyDao>(
1633
+ dao: DAO,
1634
+ ids: InferID<DAO>[],
1474
1635
  opt?: CommonDaoOptions,
1475
1636
  ): Promise<number> {
1476
1637
  return await dao.deleteByIds(ids, { ...opt, tx: this.tx })
1477
1638
  }
1478
1639
  }
1640
+
1641
+ export interface DaoWithIds<DAO extends AnyDao> {
1642
+ dao: DAO
1643
+ ids: string[]
1644
+ }
1645
+
1646
+ export interface DaoWithId<DAO extends AnyDao> {
1647
+ dao: DAO
1648
+ id: string
1649
+ }
1650
+
1651
+ export interface DaoWithRows<DAO extends AnyDao> {
1652
+ dao: DAO
1653
+ rows: InferBM<DAO>[]
1654
+ }
1655
+
1656
+ type InferBM<DAO> = DAO extends CommonDao<infer BM> ? BM : never
1657
+ // type InferDBM<DAO> = DAO extends CommonDao<any, infer DBM> ? DBM : never
1658
+ type InferID<DAO> = DAO extends CommonDao<any, any, infer ID> ? ID : never
1659
+
1660
+ export type AnyDao = CommonDao<any>
@@ -3,6 +3,7 @@ import type { ObjectWithId, StringMap } from '@naturalcycles/js-lib/types'
3
3
  import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream'
4
4
  import type {
5
5
  CommonDBOptions,
6
+ CommonDBReadOptions,
6
7
  CommonDBSaveOptions,
7
8
  CommonDBTransactionOptions,
8
9
  DBTransaction,
@@ -60,6 +61,15 @@ export class BaseCommonDB implements CommonDB {
60
61
  throw new Error('patchByQuery is not implemented')
61
62
  }
62
63
 
64
+ async patchById<ROW extends ObjectWithId>(
65
+ _table: string,
66
+ _id: string,
67
+ _patch: Partial<ROW>,
68
+ _opt?: CommonDBOptions,
69
+ ): Promise<void> {
70
+ throw new Error('patchById is not implemented')
71
+ }
72
+
63
73
  async runQuery<ROW extends ObjectWithId>(_q: DBQuery<ROW>): Promise<RunQueryResult<ROW>> {
64
74
  throw new Error('runQuery is not implemented')
65
75
  }
@@ -102,4 +112,22 @@ export class BaseCommonDB implements CommonDB {
102
112
  ): Promise<StringMap<number>> {
103
113
  throw new Error('incrementBatch is not implemented')
104
114
  }
115
+
116
+ async multiGet<ROW extends ObjectWithId>(
117
+ _map: StringMap<string[]>,
118
+ _opt?: CommonDBReadOptions,
119
+ ): Promise<StringMap<ROW[]>> {
120
+ throw new Error('multiGetByIds is not implemented')
121
+ }
122
+
123
+ async multiSave<ROW extends ObjectWithId>(
124
+ _map: StringMap<ROW[]>,
125
+ _opt?: CommonDBSaveOptions<ROW>,
126
+ ): Promise<void> {
127
+ throw new Error('multiSaveBatch is not implemented')
128
+ }
129
+
130
+ async multiDelete(_map: StringMap<string[]>, _opt?: CommonDBOptions): Promise<number> {
131
+ throw new Error('multiDeleteByIds is not implemented')
132
+ }
105
133
  }
@@ -1,5 +1,5 @@
1
1
  import type { JsonSchemaObject, JsonSchemaRootObject } from '@naturalcycles/js-lib/json-schema'
2
- import type { ObjectWithId, StringMap } from '@naturalcycles/js-lib/types'
2
+ import type { NonNegativeInteger, ObjectWithId, StringMap } from '@naturalcycles/js-lib/types'
3
3
  import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream'
4
4
  import type {
5
5
  CommonDBCreateOptions,
@@ -14,11 +14,6 @@ import type {
14
14
  } from '../db.model.js'
15
15
  import type { DBQuery } from '../query/dbQuery.js'
16
16
 
17
- export enum CommonDBType {
18
- 'document' = 'document',
19
- 'relational' = 'relational',
20
- }
21
-
22
17
  export interface CommonDB {
23
18
  /**
24
19
  * Relational databases are expected to return `null` for all missing properties.
@@ -72,6 +67,27 @@ export interface CommonDB {
72
67
  opt?: CommonDBReadOptions,
73
68
  ) => Promise<ROW[]>
74
69
 
70
+ /**
71
+ * Get rows from multiple tables at once.
72
+ * Mimics the API of some NoSQL databases like Firestore.
73
+ *
74
+ * Takes `map`, which is a map from "table name" to an array of ids.
75
+ * Example:
76
+ * {
77
+ * 'TableOne': ['id1', 'id2'],
78
+ * 'TableTwo': ['id3'],
79
+ * }
80
+ *
81
+ * Returns a map with the same keys (table names) and arrays of rows as values.
82
+ * Even if some table is not found, it will return an empty array of results for that table.
83
+ *
84
+ * @experimental
85
+ */
86
+ multiGet: <ROW extends ObjectWithId>(
87
+ idsByTable: StringMap<string[]>,
88
+ opt?: CommonDBReadOptions,
89
+ ) => Promise<StringMap<ROW[]>>
90
+
75
91
  // QUERY
76
92
  /**
77
93
  * Order by 'id' is not supported by all implementations (for example, Datastore doesn't support it).
@@ -84,7 +100,7 @@ export interface CommonDB {
84
100
  runQueryCount: <ROW extends ObjectWithId>(
85
101
  q: DBQuery<ROW>,
86
102
  opt?: CommonDBReadOptions,
87
- ) => Promise<number>
103
+ ) => Promise<NonNegativeInteger>
88
104
 
89
105
  streamQuery: <ROW extends ObjectWithId>(
90
106
  q: DBQuery<ROW>,
@@ -101,12 +117,66 @@ export interface CommonDB {
101
117
  opt?: CommonDBSaveOptions<ROW>,
102
118
  ) => Promise<void>
103
119
 
120
+ /**
121
+ * Save rows for multiple tables at once.
122
+ * Mimics the API of some NoSQL databases like Firestore.
123
+ *
124
+ * Takes `map`, which is a map from "table name" to an array of rows.
125
+ * Example:
126
+ * {
127
+ * 'TableOne': [{ id: 'id1', ... }, { id: 'id2', ... }],
128
+ * 'TableTwo': [{ id: 'id3', ... }],
129
+ * }
130
+ *
131
+ * @experimental
132
+ */
133
+ multiSave: <ROW extends ObjectWithId>(
134
+ rowsByTable: StringMap<ROW[]>,
135
+ opt?: CommonDBSaveOptions<ROW>,
136
+ ) => Promise<void>
137
+
138
+ /**
139
+ * Perform a partial update of a row by its id.
140
+ * Unlike save - doesn't require to first load the doc.
141
+ * Mimics the API of some NoSQL databases like Firestore.
142
+ *
143
+ * The object with given id has to exist, otherwise an error will be thrown.
144
+ *
145
+ * @experimental
146
+ */
147
+ patchById: <ROW extends ObjectWithId>(
148
+ table: string,
149
+ id: string,
150
+ patch: Partial<ROW>,
151
+ opt?: CommonDBOptions,
152
+ ) => Promise<void>
153
+
104
154
  // DELETE
105
155
  /**
106
156
  * Returns number of deleted items.
107
157
  * Not supported by all implementations (e.g Datastore will always return same number as number of ids).
108
158
  */
109
- deleteByIds: (table: string, ids: string[], opt?: CommonDBOptions) => Promise<number>
159
+ deleteByIds: (table: string, ids: string[], opt?: CommonDBOptions) => Promise<NonNegativeInteger>
160
+
161
+ /**
162
+ * Deletes rows from multiple tables at once.
163
+ * Mimics the API of some NoSQL databases like Firestore.
164
+ * Takes `map`, which is a map from "table name" to an array of ids to delete.
165
+ * Example:
166
+ * {
167
+ * 'TableOne': ['id1', 'id2'],
168
+ * 'TableTwo': ['id3'],
169
+ * }
170
+ *
171
+ * Returns number of deleted items.
172
+ * Not supported by all implementations (e.g Datastore will always return same number as number of ids).
173
+ *
174
+ * @experimental
175
+ */
176
+ multiDelete: (
177
+ idsByTable: StringMap<string[]>,
178
+ opt?: CommonDBOptions,
179
+ ) => Promise<NonNegativeInteger>
110
180
 
111
181
  /**
112
182
  * Returns number of deleted items.
@@ -115,7 +185,7 @@ export interface CommonDB {
115
185
  deleteByQuery: <ROW extends ObjectWithId>(
116
186
  q: DBQuery<ROW>,
117
187
  opt?: CommonDBOptions,
118
- ) => Promise<number>
188
+ ) => Promise<NonNegativeInteger>
119
189
 
120
190
  /**
121
191
  * Applies patch to all the rows that are matched by the query.
@@ -178,6 +248,11 @@ export interface CommonDB {
178
248
  ) => Promise<StringMap<number>>
179
249
  }
180
250
 
251
+ export enum CommonDBType {
252
+ 'document' = 'document',
253
+ 'relational' = 'relational',
254
+ }
255
+
181
256
  /**
182
257
  * Manifest of supported features.
183
258
  */
@@ -190,6 +265,7 @@ export interface CommonDBSupport {
190
265
  insertSaveMethod?: boolean
191
266
  updateSaveMethod?: boolean
192
267
  patchByQuery?: boolean
268
+ patchById?: boolean
193
269
  increment?: boolean
194
270
  createTable?: boolean
195
271
  tableSchemas?: boolean
@@ -198,9 +274,10 @@ export interface CommonDBSupport {
198
274
  nullValues?: boolean
199
275
  transactions?: boolean
200
276
  timeMachine?: boolean
277
+ multiTableOperations?: boolean
201
278
  }
202
279
 
203
- export const commonDBFullSupport: CommonDBSupport = {
280
+ export const commonDBFullSupport: Required<CommonDBSupport> = {
204
281
  queries: true,
205
282
  dbQueryFilter: true,
206
283
  dbQueryFilterIn: true,
@@ -209,6 +286,7 @@ export const commonDBFullSupport: CommonDBSupport = {
209
286
  insertSaveMethod: true,
210
287
  updateSaveMethod: true,
211
288
  patchByQuery: true,
289
+ patchById: true,
212
290
  increment: true,
213
291
  createTable: true,
214
292
  tableSchemas: true,
@@ -217,4 +295,5 @@ export const commonDBFullSupport: CommonDBSupport = {
217
295
  nullValues: true,
218
296
  transactions: true,
219
297
  timeMachine: true,
298
+ multiTableOperations: true,
220
299
  }
@@ -145,6 +145,20 @@ export class InMemoryDB implements CommonDB {
145
145
  return ids.map(id => this.data[table]![id] as ROW).filter(Boolean)
146
146
  }
147
147
 
148
+ async multiGet<ROW extends ObjectWithId>(
149
+ map: StringMap<string[]>,
150
+ _opt: CommonDBOptions = {},
151
+ ): Promise<StringMap<ROW[]>> {
152
+ const result: StringMap<ROW[]> = {}
153
+
154
+ for (const [tableName, ids] of _stringMapEntries(map)) {
155
+ const table = this.cfg.tablesPrefix + tableName
156
+ result[table] = ids.map(id => this.data[table]?.[id] as ROW).filter(Boolean)
157
+ }
158
+
159
+ return result
160
+ }
161
+
148
162
  async saveBatch<ROW extends ObjectWithId>(
149
163
  _table: string,
150
164
  rows: ROW[],
@@ -152,6 +166,8 @@ export class InMemoryDB implements CommonDB {
152
166
  ): Promise<void> {
153
167
  const table = this.cfg.tablesPrefix + _table
154
168
  this.data[table] ||= {}
169
+ const isInsert = opt.saveMethod === 'insert'
170
+ const isUpdate = opt.saveMethod === 'update'
155
171
 
156
172
  for (const r of rows) {
157
173
  if (!r.id) {
@@ -161,11 +177,11 @@ export class InMemoryDB implements CommonDB {
161
177
  )
162
178
  }
163
179
 
164
- if (opt.saveMethod === 'insert' && this.data[table][r.id]) {
180
+ if (isInsert && this.data[table][r.id]) {
165
181
  throw new Error(`InMemoryDB: INSERT failed, entity exists: ${table}.${r.id}`)
166
182
  }
167
183
 
168
- if (opt.saveMethod === 'update' && !this.data[table][r.id]) {
184
+ if (isUpdate && !this.data[table][r.id]) {
169
185
  throw new Error(`InMemoryDB: UPDATE failed, entity doesn't exist: ${table}.${r.id}`)
170
186
  }
171
187
 
@@ -176,6 +192,29 @@ export class InMemoryDB implements CommonDB {
176
192
  }
177
193
  }
178
194
 
195
+ async multiSave<ROW extends ObjectWithId>(
196
+ map: StringMap<ROW[]>,
197
+ opt: CommonDBSaveOptions<ROW> = {},
198
+ ): Promise<void> {
199
+ for (const [table, rows] of _stringMapEntries(map)) {
200
+ await this.saveBatch(table, rows, opt)
201
+ }
202
+ }
203
+
204
+ async patchById<ROW extends ObjectWithId>(
205
+ _table: string,
206
+ id: string,
207
+ patch: Partial<ROW>,
208
+ _opt?: CommonDBOptions,
209
+ ): Promise<void> {
210
+ const table = this.cfg.tablesPrefix + _table
211
+ _assert(
212
+ !this.data[table]?.[id],
213
+ `InMemoryDB: patchById failed, entity doesn't exist: ${table}.${id}`,
214
+ )
215
+ Object.assign(this.data[table]![id]!, patch)
216
+ }
217
+
179
218
  async deleteByQuery<ROW extends ObjectWithId>(
180
219
  q: DBQuery<ROW>,
181
220
  _opt?: CommonDBOptions,
@@ -200,6 +239,16 @@ export class InMemoryDB implements CommonDB {
200
239
  return count
201
240
  }
202
241
 
242
+ async multiDelete(map: StringMap<string[]>, _opt?: CommonDBOptions): Promise<number> {
243
+ let count = 0
244
+
245
+ for (const [table, ids] of _stringMapEntries(map)) {
246
+ count += await this.deleteByIds(table, ids, _opt)
247
+ }
248
+
249
+ return count
250
+ }
251
+
203
252
  async patchByQuery<ROW extends ObjectWithId>(
204
253
  q: DBQuery<ROW>,
205
254
  patch: Partial<ROW>,
@@ -236,7 +236,7 @@ export class DBQuery<ROW extends ObjectWithId> {
236
236
  export class RunnableDBQuery<
237
237
  BM extends BaseDBEntity,
238
238
  DBM extends BaseDBEntity = BM,
239
- ID = BM['id'],
239
+ ID extends string = BM['id'],
240
240
  > extends DBQuery<DBM> {
241
241
  /**
242
242
  * Pass `table` to override table.
@@ -15,6 +15,7 @@ import {
15
15
  const MOCK_TS_2018_06_21 = 1529539200 as UnixTimestamp
16
16
 
17
17
  export const TEST_TABLE = 'TEST_TABLE'
18
+ export const TEST_TABLE_2 = 'TEST_TABLE_2'
18
19
 
19
20
  export interface TestItemBM extends BaseDBEntity {
20
21
  k1: string