@naturalcycles/db-lib 10.17.1 → 10.18.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,12 +1,12 @@
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, BaseDBEntity, NonNegativeInteger, StringMap, UnixTimestampMillis, 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';
8
8
  import { RunnableDBQuery } from '../query/dbQuery.js';
9
- import type { CommonDaoCfg, CommonDaoCreateOptions, CommonDaoOptions, CommonDaoPatchByIdOptions, CommonDaoPatchOptions, CommonDaoReadOptions, CommonDaoSaveBatchOptions, CommonDaoSaveOptions, CommonDaoStreamDeleteOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions, CommonDaoStreamSaveOptions } from './common.dao.model.js';
9
+ import type { CommonDaoCfg, CommonDaoCreateOptions, CommonDaoOptions, CommonDaoPatchByIdOptions, CommonDaoPatchOptions, CommonDaoReadOptions, CommonDaoSaveBatchOptions, CommonDaoSaveOptions, CommonDaoStreamDeleteOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions, CommonDaoStreamSaveOptions, DaoWithId, DaoWithIds, DaoWithRows } from './common.dao.model.js';
10
10
  /**
11
11
  * Lowest common denominator API between supported Databases.
12
12
  *
@@ -167,6 +167,22 @@ 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
+ id(id: string): DaoWithId;
171
+ ids(ids: string[]): DaoWithIds;
172
+ rows(rows: BM[]): DaoWithRows;
173
+ /**
174
+ * Very @experimental.
175
+ */
176
+ static multiGetById<T>(inputs: DaoWithId[], opt?: CommonDaoReadOptions): Promise<T>;
177
+ /**
178
+ * Very @experimental.
179
+ */
180
+ static multiGetByIds(inputs: DaoWithIds[], opt?: CommonDaoReadOptions): Promise<StringMap<unknown[]>>;
181
+ /**
182
+ * Very @experimental.
183
+ */
184
+ static multiDeleteByIds(inputs: DaoWithIds[], _opt?: CommonDaoOptions): Promise<NonNegativeInteger>;
185
+ static multiSaveBatch(inputs: DaoWithRows[], opt?: CommonDaoSaveBatchOptions<any>): Promise<void>;
170
186
  createTransaction(opt?: CommonDBTransactionOptions): Promise<CommonDaoTransaction>;
171
187
  runInTransaction<T = void>(fn: CommonDaoTransactionFn<T>, opt?: CommonDBTransactionOptions): Promise<T>;
172
188
  /**
@@ -981,6 +981,89 @@ export class CommonDao {
981
981
  async ping() {
982
982
  await this.cfg.db.ping();
983
983
  }
984
+ id(id) {
985
+ return {
986
+ dao: this,
987
+ id,
988
+ };
989
+ }
990
+ ids(ids) {
991
+ return {
992
+ dao: this,
993
+ ids,
994
+ };
995
+ }
996
+ rows(rows) {
997
+ return {
998
+ dao: this,
999
+ rows,
1000
+ };
1001
+ }
1002
+ /**
1003
+ * Very @experimental.
1004
+ */
1005
+ static async multiGetById(inputs, opt = {}) {
1006
+ const inputs2 = inputs.map(input => ({
1007
+ dao: input.dao,
1008
+ ids: [input.id],
1009
+ }));
1010
+ const rowsByTable = await CommonDao.multiGetByIds(inputs2, opt);
1011
+ const results = inputs.map(({ dao }) => {
1012
+ const { table } = dao.cfg;
1013
+ return rowsByTable[table]?.[0] || null;
1014
+ });
1015
+ return results;
1016
+ }
1017
+ /**
1018
+ * Very @experimental.
1019
+ */
1020
+ static async multiGetByIds(inputs, opt = {}) {
1021
+ if (!inputs.length)
1022
+ return {};
1023
+ const { db } = inputs[0].dao.cfg;
1024
+ const idsByTable = {};
1025
+ for (const { dao, ids } of inputs) {
1026
+ const { table } = dao.cfg;
1027
+ idsByTable[table] = ids;
1028
+ }
1029
+ // todo: support tx
1030
+ const dbmsByTable = await db.multiGetByIds(idsByTable, opt);
1031
+ const bmsByTable = {};
1032
+ await pMap(inputs, async ({ dao }) => {
1033
+ const { table } = dao.cfg;
1034
+ let dbms = dbmsByTable[table] || [];
1035
+ if (dao.cfg.hooks.afterLoad && dbms.length) {
1036
+ dbms = (await pMap(dbms, async (dbm) => await dao.cfg.hooks.afterLoad(dbm))).filter(_isTruthy);
1037
+ }
1038
+ bmsByTable[table] = await dao.dbmsToBM(dbms, opt);
1039
+ });
1040
+ return bmsByTable;
1041
+ }
1042
+ /**
1043
+ * Very @experimental.
1044
+ */
1045
+ static async multiDeleteByIds(inputs, _opt = {}) {
1046
+ if (!inputs.length)
1047
+ return 0;
1048
+ const { db } = inputs[0].dao.cfg;
1049
+ const idsByTable = {};
1050
+ for (const { dao, ids } of inputs) {
1051
+ idsByTable[dao.cfg.table] = ids;
1052
+ }
1053
+ return await db.multiDeleteByIds(idsByTable);
1054
+ }
1055
+ static async multiSaveBatch(inputs, opt = {}) {
1056
+ if (!inputs.length)
1057
+ return;
1058
+ const { db } = inputs[0].dao.cfg;
1059
+ const dbmsByTable = {};
1060
+ await pMap(inputs, async ({ dao, rows }) => {
1061
+ const { table } = dao.cfg;
1062
+ rows.forEach(bm => dao.assignIdCreatedUpdated(bm, opt));
1063
+ dbmsByTable[table] = await dao.bmsToDBM(rows, opt);
1064
+ });
1065
+ await db.multiSaveBatch(dbmsByTable);
1066
+ }
984
1067
  async createTransaction(opt) {
985
1068
  const tx = await this.cfg.db.createTransaction(opt);
986
1069
  return new CommonDaoTransaction(tx, this.cfg.logger);
@@ -1,10 +1,11 @@
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, ObjectWithId, 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';
8
+ import type { CommonDao } from './common.dao.js';
8
9
  export interface CommonDaoHooks<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID = BM['id']> {
9
10
  /**
10
11
  * Allows to override the id generation function.
@@ -197,10 +198,7 @@ export interface CommonDaoIndex<DBM extends BaseDBEntity> {
197
198
  * Name of the property to index.
198
199
  */
199
200
  name: keyof DBM;
200
- /**
201
- * Defaults to ['asc']
202
- */
203
- order?: CommonDaoIndexOrder[];
201
+ order: CommonDaoIndexOrder[];
204
202
  }
205
203
  export type CommonDaoIndexOrder = 'asc' | 'desc' | 'array-contains';
206
204
  /**
@@ -335,8 +333,15 @@ export interface CommonDaoStreamOptions<IN> extends CommonDaoReadOptions, Transf
335
333
  chunkConcurrency?: number;
336
334
  }
337
335
  export type CommonDaoCreateOptions = CommonDBCreateOptions;
338
- export interface OnValidationTimeData {
339
- tookMillis: NumberOfMilliseconds;
340
- table: string;
341
- obj: any;
336
+ export interface DaoWithIds {
337
+ dao: CommonDao<any>;
338
+ ids: string[];
339
+ }
340
+ export interface DaoWithId {
341
+ dao: CommonDao<any>;
342
+ id: string;
343
+ }
344
+ export interface DaoWithRows<ROW extends ObjectWithId = ObjectWithId> {
345
+ dao: CommonDao<any>;
346
+ rows: ROW[];
342
347
  }
@@ -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
+ multiGetByIds<ROW extends ObjectWithId>(_map: StringMap<string[]>, _opt?: CommonDBReadOptions): Promise<StringMap<ROW[]>>;
32
+ multiSaveBatch<ROW extends ObjectWithId>(_map: StringMap<ROW[]>, _opt?: CommonDBSaveOptions<ROW>): Promise<void>;
33
+ multiDeleteByIds(_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 multiGetByIds(_map, _opt) {
61
+ throw new Error('multiGetByIds is not implemented');
62
+ }
63
+ async multiSaveBatch(_map, _opt) {
64
+ throw new Error('multiSaveBatch is not implemented');
65
+ }
66
+ async multiDeleteByIds(_map, _opt) {
67
+ throw new Error('multiDeleteByIds is not implemented');
68
+ }
57
69
  }
@@ -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 { CommonDBCreateOptions, CommonDBOptions, CommonDBReadOptions, CommonDBSaveOptions, CommonDBStreamOptions, CommonDBTransactionOptions, DBTransaction, DBTransactionFn, RunQueryResult } from '../db.model.js';
5
5
  import type { DBQuery } from '../query/dbQuery.js';
@@ -7,6 +7,10 @@ export declare enum CommonDBType {
7
7
  'document' = "document",
8
8
  'relational' = "relational"
9
9
  }
10
+ /**
11
+ * A tuple that contains the table name and the id.
12
+ */
13
+ export type CommonDBKey = [table: string, id: string];
10
14
  export interface CommonDB {
11
15
  /**
12
16
  * Relational databases are expected to return `null` for all missing properties.
@@ -44,26 +48,83 @@ export interface CommonDB {
44
48
  * (Such limitation exists because Datastore doesn't support it).
45
49
  */
46
50
  getByIds: <ROW extends ObjectWithId>(table: string, ids: string[], opt?: CommonDBReadOptions) => Promise<ROW[]>;
51
+ /**
52
+ * Get rows from multiple tables at once.
53
+ * Mimics the API of some NoSQL databases like Firestore.
54
+ *
55
+ * Takes `map`, which is a map from "table name" to an array of ids.
56
+ * Example:
57
+ * {
58
+ * 'TableOne': ['id1', 'id2'],
59
+ * 'TableTwo': ['id3'],
60
+ * }
61
+ *
62
+ * Returns a map with the same keys (table names) and arrays of rows as values.
63
+ * Even if some table is not found, it will return an empty array of results for that table.
64
+ *
65
+ * @experimental
66
+ */
67
+ multiGetByIds: <ROW extends ObjectWithId>(idsByTable: StringMap<string[]>, opt?: CommonDBReadOptions) => Promise<StringMap<ROW[]>>;
47
68
  /**
48
69
  * Order by 'id' is not supported by all implementations (for example, Datastore doesn't support it).
49
70
  */
50
71
  runQuery: <ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBReadOptions) => Promise<RunQueryResult<ROW>>;
51
- runQueryCount: <ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBReadOptions) => Promise<number>;
72
+ runQueryCount: <ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBReadOptions) => Promise<NonNegativeInteger>;
52
73
  streamQuery: <ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBStreamOptions) => ReadableTyped<ROW>;
53
74
  /**
54
75
  * rows can have missing ids only if DB supports auto-generating them (like mysql auto_increment).
55
76
  */
56
77
  saveBatch: <ROW extends ObjectWithId>(table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>) => Promise<void>;
57
78
  /**
79
+ * Save rows for multiple tables at once.
80
+ * Mimics the API of some NoSQL databases like Firestore.
81
+ *
82
+ * Takes `map`, which is a map from "table name" to an array of rows.
83
+ * Example:
84
+ * {
85
+ * 'TableOne': [{ id: 'id1', ... }, { id: 'id2', ... }],
86
+ * 'TableTwo': [{ id: 'id3', ... }],
87
+ * }
88
+ *
89
+ * @experimental
90
+ */
91
+ multiSaveBatch: <ROW extends ObjectWithId>(rowsByTable: StringMap<ROW[]>, opt?: CommonDBSaveOptions<ROW>) => Promise<void>;
92
+ /**
93
+ * Perform a partial update of a row by its id.
94
+ * Unlike save - doesn't require to first load the doc.
95
+ * Mimics the API of some NoSQL databases like Firestore.
96
+ *
97
+ * The object with given id has to exist, otherwise an error will be thrown.
98
+ *
99
+ * @experimental
100
+ */
101
+ patchById: <ROW extends ObjectWithId>(table: string, id: string, patch: Partial<ROW>, opt?: CommonDBOptions) => Promise<void>;
102
+ /**
103
+ * Returns number of deleted items.
104
+ * Not supported by all implementations (e.g Datastore will always return same number as number of ids).
105
+ */
106
+ deleteByIds: (table: string, ids: string[], opt?: CommonDBOptions) => Promise<NonNegativeInteger>;
107
+ /**
108
+ * Deletes rows from multiple tables at once.
109
+ * Mimics the API of some NoSQL databases like Firestore.
110
+ * Takes `map`, which is a map from "table name" to an array of ids to delete.
111
+ * Example:
112
+ * {
113
+ * 'TableOne': ['id1', 'id2'],
114
+ * 'TableTwo': ['id3'],
115
+ * }
116
+ *
58
117
  * Returns number of deleted items.
59
118
  * Not supported by all implementations (e.g Datastore will always return same number as number of ids).
119
+ *
120
+ * @experimental
60
121
  */
61
- deleteByIds: (table: string, ids: string[], opt?: CommonDBOptions) => Promise<number>;
122
+ multiDeleteByIds: (idsByTable: StringMap<string[]>, opt?: CommonDBOptions) => Promise<NonNegativeInteger>;
62
123
  /**
63
124
  * Returns number of deleted items.
64
125
  * Not supported by all implementations (e.g Datastore will always return same number as number of ids).
65
126
  */
66
- deleteByQuery: <ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions) => Promise<number>;
127
+ deleteByQuery: <ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions) => Promise<NonNegativeInteger>;
67
128
  /**
68
129
  * Applies patch to all the rows that are matched by the query.
69
130
  *
@@ -123,6 +184,7 @@ export interface CommonDBSupport {
123
184
  insertSaveMethod?: boolean;
124
185
  updateSaveMethod?: boolean;
125
186
  patchByQuery?: boolean;
187
+ patchById?: boolean;
126
188
  increment?: boolean;
127
189
  createTable?: boolean;
128
190
  tableSchemas?: boolean;
@@ -131,5 +193,6 @@ export interface CommonDBSupport {
131
193
  nullValues?: boolean;
132
194
  transactions?: boolean;
133
195
  timeMachine?: boolean;
196
+ multiTableOperations?: boolean;
134
197
  }
135
- export declare const commonDBFullSupport: CommonDBSupport;
198
+ 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
+ multiGetByIds<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
+ multiSaveBatch<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
+ multiDeleteByIds(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 multiGetByIds(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 multiSaveBatch(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 multiDeleteByIds(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;
@@ -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.1",
4
+ "version": "10.18.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@naturalcycles/nodejs-lib": "^15"
@@ -3,7 +3,7 @@ import type { AppError, ErrorMode } from '@naturalcycles/js-lib/error'
3
3
  import type { CommonLogger } from '@naturalcycles/js-lib/log'
4
4
  import type {
5
5
  BaseDBEntity,
6
- NumberOfMilliseconds,
6
+ ObjectWithId,
7
7
  Promisable,
8
8
  UnixTimestamp,
9
9
  } from '@naturalcycles/js-lib/types'
@@ -13,6 +13,7 @@ import type {
13
13
  } from '@naturalcycles/nodejs-lib/stream'
14
14
  import type { CommonDB } from '../commondb/common.db.js'
15
15
  import type { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions } from '../db.model.js'
16
+ import type { CommonDao } from './common.dao.js'
16
17
 
17
18
  export interface CommonDaoHooks<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID = BM['id']> {
18
19
  /**
@@ -239,10 +240,7 @@ export interface CommonDaoIndex<DBM extends BaseDBEntity> {
239
240
  * Name of the property to index.
240
241
  */
241
242
  name: keyof DBM
242
- /**
243
- * Defaults to ['asc']
244
- */
245
- order?: CommonDaoIndexOrder[]
243
+ order: CommonDaoIndexOrder[]
246
244
  }
247
245
 
248
246
  export type CommonDaoIndexOrder = 'asc' | 'desc' | 'array-contains'
@@ -407,8 +405,17 @@ export interface CommonDaoStreamOptions<IN>
407
405
 
408
406
  export type CommonDaoCreateOptions = CommonDBCreateOptions
409
407
 
410
- export interface OnValidationTimeData {
411
- tookMillis: NumberOfMilliseconds
412
- table: string
413
- obj: any
408
+ export interface DaoWithIds {
409
+ dao: CommonDao<any>
410
+ ids: string[]
411
+ }
412
+
413
+ export interface DaoWithId {
414
+ dao: CommonDao<any>
415
+ id: string
416
+ }
417
+
418
+ export interface DaoWithRows<ROW extends ObjectWithId = ObjectWithId> {
419
+ dao: CommonDao<any>
420
+ rows: ROW[]
414
421
  }
@@ -17,6 +17,7 @@ import { _truncate } from '@naturalcycles/js-lib/string/string.util.js'
17
17
  import type {
18
18
  AsyncIndexedMapper,
19
19
  BaseDBEntity,
20
+ NonNegativeInteger,
20
21
  ObjectWithId,
21
22
  StringMap,
22
23
  UnixTimestampMillis,
@@ -51,6 +52,9 @@ import type {
51
52
  CommonDaoStreamForEachOptions,
52
53
  CommonDaoStreamOptions,
53
54
  CommonDaoStreamSaveOptions,
55
+ DaoWithId,
56
+ DaoWithIds,
57
+ DaoWithRows,
54
58
  } from './common.dao.model.js'
55
59
  import { CommonDaoLogLevel } from './common.dao.model.js'
56
60
 
@@ -1257,6 +1261,113 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM, I
1257
1261
  await this.cfg.db.ping()
1258
1262
  }
1259
1263
 
1264
+ id(id: string): DaoWithId {
1265
+ return {
1266
+ dao: this,
1267
+ id,
1268
+ }
1269
+ }
1270
+
1271
+ ids(ids: string[]): DaoWithIds {
1272
+ return {
1273
+ dao: this,
1274
+ ids,
1275
+ }
1276
+ }
1277
+
1278
+ rows(rows: BM[]): DaoWithRows {
1279
+ return {
1280
+ dao: this,
1281
+ rows,
1282
+ }
1283
+ }
1284
+
1285
+ /**
1286
+ * Very @experimental.
1287
+ */
1288
+ static async multiGetById<T>(inputs: DaoWithId[], opt: CommonDaoReadOptions = {}): Promise<T> {
1289
+ const inputs2 = inputs.map(input => ({
1290
+ dao: input.dao,
1291
+ ids: [input.id],
1292
+ }))
1293
+
1294
+ const rowsByTable = await CommonDao.multiGetByIds(inputs2, opt)
1295
+ const results: any[] = inputs.map(({ dao }) => {
1296
+ const { table } = dao.cfg
1297
+ return rowsByTable[table]?.[0] || null
1298
+ })
1299
+
1300
+ return results as T
1301
+ }
1302
+
1303
+ /**
1304
+ * Very @experimental.
1305
+ */
1306
+ static async multiGetByIds(
1307
+ inputs: DaoWithIds[],
1308
+ opt: CommonDaoReadOptions = {},
1309
+ ): Promise<StringMap<unknown[]>> {
1310
+ if (!inputs.length) return {}
1311
+ const { db } = inputs[0]!.dao.cfg
1312
+ const idsByTable: StringMap<string[]> = {}
1313
+ for (const { dao, ids } of inputs) {
1314
+ const { table } = dao.cfg
1315
+ idsByTable[table] = ids
1316
+ }
1317
+
1318
+ // todo: support tx
1319
+ const dbmsByTable = await db.multiGetByIds(idsByTable, opt)
1320
+ const bmsByTable: StringMap<unknown[]> = {}
1321
+
1322
+ await pMap(inputs, async ({ dao }) => {
1323
+ const { table } = dao.cfg
1324
+ let dbms = dbmsByTable[table] || []
1325
+
1326
+ if (dao.cfg.hooks!.afterLoad && dbms.length) {
1327
+ dbms = (await pMap(dbms, async dbm => await dao.cfg.hooks!.afterLoad!(dbm))).filter(
1328
+ _isTruthy,
1329
+ )
1330
+ }
1331
+
1332
+ bmsByTable[table] = await dao.dbmsToBM(dbms, opt)
1333
+ })
1334
+
1335
+ return bmsByTable
1336
+ }
1337
+
1338
+ /**
1339
+ * Very @experimental.
1340
+ */
1341
+ static async multiDeleteByIds(
1342
+ inputs: DaoWithIds[],
1343
+ _opt: CommonDaoOptions = {},
1344
+ ): Promise<NonNegativeInteger> {
1345
+ if (!inputs.length) return 0
1346
+ const { db } = inputs[0]!.dao.cfg
1347
+ const idsByTable: StringMap<string[]> = {}
1348
+ for (const { dao, ids } of inputs) {
1349
+ idsByTable[dao.cfg.table] = ids
1350
+ }
1351
+
1352
+ return await db.multiDeleteByIds(idsByTable)
1353
+ }
1354
+
1355
+ static async multiSaveBatch(
1356
+ inputs: DaoWithRows[],
1357
+ opt: CommonDaoSaveBatchOptions<any> = {},
1358
+ ): Promise<void> {
1359
+ if (!inputs.length) return
1360
+ const { db } = inputs[0]!.dao.cfg
1361
+ const dbmsByTable: StringMap<any[]> = {}
1362
+ await pMap(inputs, async ({ dao, rows }) => {
1363
+ const { table } = dao.cfg
1364
+ rows.forEach(bm => dao.assignIdCreatedUpdated(bm, opt))
1365
+ dbmsByTable[table] = await dao.bmsToDBM(rows, opt)
1366
+ })
1367
+
1368
+ await db.multiSaveBatch(dbmsByTable)
1369
+ }
1370
+
1260
1371
  async createTransaction(opt?: CommonDBTransactionOptions): Promise<CommonDaoTransaction> {
1261
1372
  const tx = await this.cfg.db.createTransaction(opt)
1262
1373
  return new CommonDaoTransaction(tx, this.cfg.logger!)
@@ -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 multiGetByIds<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 multiSaveBatch<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 multiDeleteByIds(_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,
@@ -19,6 +19,11 @@ export enum CommonDBType {
19
19
  'relational' = 'relational',
20
20
  }
21
21
 
22
+ /**
23
+ * A tuple that contains the table name and the id.
24
+ */
25
+ export type CommonDBKey = [table: string, id: string]
26
+
22
27
  export interface CommonDB {
23
28
  /**
24
29
  * Relational databases are expected to return `null` for all missing properties.
@@ -72,6 +77,27 @@ export interface CommonDB {
72
77
  opt?: CommonDBReadOptions,
73
78
  ) => Promise<ROW[]>
74
79
 
80
+ /**
81
+ * Get rows from multiple tables at once.
82
+ * Mimics the API of some NoSQL databases like Firestore.
83
+ *
84
+ * Takes `map`, which is a map from "table name" to an array of ids.
85
+ * Example:
86
+ * {
87
+ * 'TableOne': ['id1', 'id2'],
88
+ * 'TableTwo': ['id3'],
89
+ * }
90
+ *
91
+ * Returns a map with the same keys (table names) and arrays of rows as values.
92
+ * Even if some table is not found, it will return an empty array of results for that table.
93
+ *
94
+ * @experimental
95
+ */
96
+ multiGetByIds: <ROW extends ObjectWithId>(
97
+ idsByTable: StringMap<string[]>,
98
+ opt?: CommonDBReadOptions,
99
+ ) => Promise<StringMap<ROW[]>>
100
+
75
101
  // QUERY
76
102
  /**
77
103
  * Order by 'id' is not supported by all implementations (for example, Datastore doesn't support it).
@@ -84,7 +110,7 @@ export interface CommonDB {
84
110
  runQueryCount: <ROW extends ObjectWithId>(
85
111
  q: DBQuery<ROW>,
86
112
  opt?: CommonDBReadOptions,
87
- ) => Promise<number>
113
+ ) => Promise<NonNegativeInteger>
88
114
 
89
115
  streamQuery: <ROW extends ObjectWithId>(
90
116
  q: DBQuery<ROW>,
@@ -101,12 +127,66 @@ export interface CommonDB {
101
127
  opt?: CommonDBSaveOptions<ROW>,
102
128
  ) => Promise<void>
103
129
 
130
+ /**
131
+ * Save rows for multiple tables at once.
132
+ * Mimics the API of some NoSQL databases like Firestore.
133
+ *
134
+ * Takes `map`, which is a map from "table name" to an array of rows.
135
+ * Example:
136
+ * {
137
+ * 'TableOne': [{ id: 'id1', ... }, { id: 'id2', ... }],
138
+ * 'TableTwo': [{ id: 'id3', ... }],
139
+ * }
140
+ *
141
+ * @experimental
142
+ */
143
+ multiSaveBatch: <ROW extends ObjectWithId>(
144
+ rowsByTable: StringMap<ROW[]>,
145
+ opt?: CommonDBSaveOptions<ROW>,
146
+ ) => Promise<void>
147
+
148
+ /**
149
+ * Perform a partial update of a row by its id.
150
+ * Unlike save - doesn't require to first load the doc.
151
+ * Mimics the API of some NoSQL databases like Firestore.
152
+ *
153
+ * The object with given id has to exist, otherwise an error will be thrown.
154
+ *
155
+ * @experimental
156
+ */
157
+ patchById: <ROW extends ObjectWithId>(
158
+ table: string,
159
+ id: string,
160
+ patch: Partial<ROW>,
161
+ opt?: CommonDBOptions,
162
+ ) => Promise<void>
163
+
104
164
  // DELETE
105
165
  /**
106
166
  * Returns number of deleted items.
107
167
  * Not supported by all implementations (e.g Datastore will always return same number as number of ids).
108
168
  */
109
- deleteByIds: (table: string, ids: string[], opt?: CommonDBOptions) => Promise<number>
169
+ deleteByIds: (table: string, ids: string[], opt?: CommonDBOptions) => Promise<NonNegativeInteger>
170
+
171
+ /**
172
+ * Deletes rows from multiple tables at once.
173
+ * Mimics the API of some NoSQL databases like Firestore.
174
+ * Takes `map`, which is a map from "table name" to an array of ids to delete.
175
+ * Example:
176
+ * {
177
+ * 'TableOne': ['id1', 'id2'],
178
+ * 'TableTwo': ['id3'],
179
+ * }
180
+ *
181
+ * Returns number of deleted items.
182
+ * Not supported by all implementations (e.g Datastore will always return same number as number of ids).
183
+ *
184
+ * @experimental
185
+ */
186
+ multiDeleteByIds: (
187
+ idsByTable: StringMap<string[]>,
188
+ opt?: CommonDBOptions,
189
+ ) => Promise<NonNegativeInteger>
110
190
 
111
191
  /**
112
192
  * Returns number of deleted items.
@@ -115,7 +195,7 @@ export interface CommonDB {
115
195
  deleteByQuery: <ROW extends ObjectWithId>(
116
196
  q: DBQuery<ROW>,
117
197
  opt?: CommonDBOptions,
118
- ) => Promise<number>
198
+ ) => Promise<NonNegativeInteger>
119
199
 
120
200
  /**
121
201
  * Applies patch to all the rows that are matched by the query.
@@ -190,6 +270,7 @@ export interface CommonDBSupport {
190
270
  insertSaveMethod?: boolean
191
271
  updateSaveMethod?: boolean
192
272
  patchByQuery?: boolean
273
+ patchById?: boolean
193
274
  increment?: boolean
194
275
  createTable?: boolean
195
276
  tableSchemas?: boolean
@@ -198,9 +279,10 @@ export interface CommonDBSupport {
198
279
  nullValues?: boolean
199
280
  transactions?: boolean
200
281
  timeMachine?: boolean
282
+ multiTableOperations?: boolean
201
283
  }
202
284
 
203
- export const commonDBFullSupport: CommonDBSupport = {
285
+ export const commonDBFullSupport: Required<CommonDBSupport> = {
204
286
  queries: true,
205
287
  dbQueryFilter: true,
206
288
  dbQueryFilterIn: true,
@@ -209,6 +291,7 @@ export const commonDBFullSupport: CommonDBSupport = {
209
291
  insertSaveMethod: true,
210
292
  updateSaveMethod: true,
211
293
  patchByQuery: true,
294
+ patchById: true,
212
295
  increment: true,
213
296
  createTable: true,
214
297
  tableSchemas: true,
@@ -217,4 +300,5 @@ export const commonDBFullSupport: CommonDBSupport = {
217
300
  nullValues: true,
218
301
  transactions: true,
219
302
  timeMachine: true,
303
+ multiTableOperations: true,
220
304
  }
@@ -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 multiGetByIds<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 multiSaveBatch<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 multiDeleteByIds(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>,
@@ -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