@naturalcycles/db-lib 8.36.1 → 8.38.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.
@@ -24,7 +24,7 @@ export declare class CacheDB extends BaseCommonDB implements CommonDB {
24
24
  createTable<ROW extends ObjectWithId>(table: string, schema: JsonSchemaObject<ROW>, opt?: CacheDBCreateOptions): Promise<void>;
25
25
  getByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][], opt?: CacheDBSaveOptions<ROW>): Promise<ROW[]>;
26
26
  deleteByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][], opt?: CacheDBOptions): Promise<number>;
27
- saveBatch<ROW extends ObjectWithId>(table: string, rows: ROW[], opt?: CacheDBSaveOptions<ROW>): Promise<void>;
27
+ saveBatch<ROW extends Partial<ObjectWithId>>(table: string, rows: ROW[], opt?: CacheDBSaveOptions<ROW>): Promise<void>;
28
28
  runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CacheDBSaveOptions<ROW>): Promise<RunQueryResult<ROW>>;
29
29
  runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CacheDBOptions): Promise<number>;
30
30
  streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CacheDBStreamOptions): Readable;
@@ -47,7 +47,7 @@ export interface CacheDBOptions {
47
47
  */
48
48
  onlyCache?: boolean;
49
49
  }
50
- export interface CacheDBSaveOptions<ROW extends ObjectWithId> extends CacheDBOptions, CommonDBSaveOptions<ROW> {
50
+ export interface CacheDBSaveOptions<ROW extends Partial<ObjectWithId>> extends CacheDBOptions, CommonDBSaveOptions<ROW> {
51
51
  }
52
52
  export interface CacheDBStreamOptions extends CacheDBOptions, CommonDBStreamOptions {
53
53
  }
@@ -22,7 +22,7 @@ export declare class FileDB extends BaseCommonDB implements CommonDB {
22
22
  ping(): Promise<void>;
23
23
  getTables(): Promise<string[]>;
24
24
  getByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][], _opt?: CommonDBOptions): Promise<ROW[]>;
25
- saveBatch<ROW extends ObjectWithId>(table: string, rows: ROW[], _opt?: CommonDBSaveOptions<ROW>): Promise<void>;
25
+ saveBatch<ROW extends Partial<ObjectWithId>>(table: string, rows: ROW[], _opt?: CommonDBSaveOptions<ROW>): Promise<void>;
26
26
  /**
27
27
  * Implementation is optimized for loading/saving _whole files_.
28
28
  */
@@ -46,6 +46,7 @@ class FileDB extends __1.BaseCommonDB {
46
46
  // 2. Merge with new data (using ids)
47
47
  let saved = 0;
48
48
  rows.forEach(r => {
49
+ (0, js_lib_1._assert)(r.id, 'FileDB: row.id is required');
49
50
  if (!(0, js_lib_1._deepEquals)(byId[r.id], r)) {
50
51
  byId[r.id] = r;
51
52
  saved++;
@@ -53,7 +53,7 @@ export declare class InMemoryDB implements CommonDB {
53
53
  getTableSchema<ROW extends ObjectWithId>(_table: string): Promise<JsonSchemaRootObject<ROW>>;
54
54
  createTable<ROW extends ObjectWithId>(_table: string, _schema: JsonSchemaObject<ROW>, opt?: CommonDBCreateOptions): Promise<void>;
55
55
  getByIds<ROW extends ObjectWithId>(_table: string, ids: ROW['id'][], _opt?: CommonDBOptions): Promise<ROW[]>;
56
- saveBatch<ROW extends ObjectWithId>(_table: string, rows: ROW[], _opt?: CommonDBSaveOptions<ROW>): Promise<void>;
56
+ saveBatch<ROW extends Partial<ObjectWithId>>(_table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
57
57
  deleteByIds<ROW extends ObjectWithId>(_table: string, ids: ROW['id'][], _opt?: CommonDBOptions): Promise<number>;
58
58
  deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<number>;
59
59
  runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<RunQueryResult<ROW>>;
@@ -73,25 +73,25 @@ class InMemoryDB {
73
73
  (_a = this.data)[table] || (_a[table] = {});
74
74
  return ids.map(id => this.data[table][id]).filter(Boolean);
75
75
  }
76
- async saveBatch(_table, rows, _opt) {
76
+ async saveBatch(_table, rows, opt = {}) {
77
77
  var _a;
78
78
  const table = this.cfg.tablesPrefix + _table;
79
79
  (_a = this.data)[table] || (_a[table] = {});
80
80
  rows.forEach(r => {
81
81
  if (!r.id) {
82
82
  this.cfg.logger?.warn({ rows });
83
- throw new Error(`InMemoryDB: id doesn't exist for row`);
83
+ throw new Error(`InMemoryDB doesn't support id auto-generation in saveBatch, row without id was given`);
84
+ }
85
+ if (opt.saveMethod === 'insert' && this.data[table][r.id]) {
86
+ throw new Error(`InMemoryDB: INSERT failed, entity exists: ${table}.${r.id}`);
87
+ }
88
+ if (opt.saveMethod === 'update' && !this.data[table][r.id]) {
89
+ throw new Error(`InMemoryDB: UPDATE failed, entity doesn't exist: ${table}.${r.id}`);
84
90
  }
85
91
  // JSON parse/stringify (deep clone) is to:
86
92
  // 1. Not store values "by reference" (avoid mutation bugs)
87
93
  // 2. Simulate real DB that would do something like that in a transport layer anyway
88
94
  this.data[table][r.id] = JSON.parse(JSON.stringify(r), nodejs_lib_1.bufferReviver);
89
- // special treatment for Buffers (assign them raw, without JSON parse/stringify)
90
- // Object.entries(r).forEach(([k, v]) => {
91
- // if (Buffer.isBuffer(v)) {
92
- // this.data[table]![r.id]![k] = v
93
- // }
94
- // })
95
95
  });
96
96
  }
97
97
  async deleteByIds(_table, ids, _opt) {
@@ -18,7 +18,7 @@ export declare class BaseCommonDB implements CommonDB {
18
18
  getByIds<ROW extends ObjectWithId>(_table: string, _ids: ROW['id'][]): Promise<ROW[]>;
19
19
  runQuery<ROW extends ObjectWithId>(_q: DBQuery<ROW>): Promise<RunQueryResult<ROW>>;
20
20
  runQueryCount<ROW extends ObjectWithId>(_q: DBQuery<ROW>): Promise<number>;
21
- saveBatch<ROW extends ObjectWithId>(_table: string, _rows: ROW[], _opt?: CommonDBSaveOptions<ROW>): Promise<void>;
21
+ saveBatch<ROW extends Partial<ObjectWithId>>(_table: string, _rows: ROW[], _opt?: CommonDBSaveOptions<ROW>): Promise<void>;
22
22
  streamQuery<ROW extends ObjectWithId>(_q: DBQuery<ROW>): ReadableTyped<ROW>;
23
23
  /**
24
24
  * Naive implementation.
@@ -38,7 +38,10 @@ export interface CommonDB {
38
38
  runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions): Promise<RunQueryResult<ROW>>;
39
39
  runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions): Promise<number>;
40
40
  streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBStreamOptions): ReadableTyped<ROW>;
41
- saveBatch<ROW extends ObjectWithId>(table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
41
+ /**
42
+ * rows can have missing ids only if DB supports auto-generating them (like mysql auto_increment).
43
+ */
44
+ saveBatch<ROW extends Partial<ObjectWithId>>(table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
42
45
  /**
43
46
  * Returns number of deleted items.
44
47
  * Not supported by all implementations (e.g Datastore will always return same number as number of ids).
@@ -1,4 +1,4 @@
1
- import { AsyncMapper, JsonSchemaObject, JsonSchemaRootObject, ObjectWithId, Saved } from '@naturalcycles/js-lib';
1
+ import { AsyncMapper, JsonSchemaObject, JsonSchemaRootObject, ObjectWithId, Saved, Unsaved } from '@naturalcycles/js-lib';
2
2
  import { AjvSchema, ObjectSchemaTyped, ReadableTyped } from '@naturalcycles/nodejs-lib';
3
3
  import { DBModelType, RunQueryResult } from '../db.model';
4
4
  import { DBQuery, RunnableDBQuery } from '../query/dbQuery';
@@ -35,6 +35,7 @@ export declare class CommonDao<BM extends Partial<ObjectWithId<ID>>, DBM extends
35
35
  * Throws if readOnly is true
36
36
  */
37
37
  private requireObjectMutability;
38
+ private ensureUniqueId;
38
39
  getBy(by: keyof DBM, value: any, limit?: number, opt?: CommonDaoOptions): Promise<Saved<BM>[]>;
39
40
  getOneBy(by: keyof DBM, value: any, opt?: CommonDaoOptions): Promise<Saved<BM> | null>;
40
41
  getAll(opt?: CommonDaoOptions): Promise<Saved<BM>[]>;
@@ -81,13 +82,11 @@ export declare class CommonDao<BM extends Partial<ObjectWithId<ID>>, DBM extends
81
82
  */
82
83
  assignIdCreatedUpdated(obj: DBM, opt?: CommonDaoOptions): DBM;
83
84
  assignIdCreatedUpdated(obj: BM, opt?: CommonDaoOptions): Saved<BM>;
85
+ assignIdCreatedUpdated(obj: Unsaved<BM>, opt?: CommonDaoOptions): Saved<BM>;
84
86
  /**
85
87
  * Mutates with id, created, updated
86
88
  */
87
- save(bm: BM, opt?: CommonDaoSaveOptions<DBM>): Promise<Saved<BM>>;
88
- private ensureImmutableDontExist;
89
- private ensureUniqueId;
90
- private throwIfObjectExists;
89
+ save(bm: Unsaved<BM>, opt?: CommonDaoSaveOptions<DBM>): Promise<Saved<BM>>;
91
90
  /**
92
91
  * Loads the row by id.
93
92
  * Creates the row (via this.create()) if it doesn't exist
@@ -99,7 +98,7 @@ export declare class CommonDao<BM extends Partial<ObjectWithId<ID>>, DBM extends
99
98
  patch(id: ID, patch: Partial<BM>, opt?: CommonDaoSaveOptions<DBM>): Promise<Saved<BM>>;
100
99
  patchAsDBM(id: ID, patch: Partial<DBM>, opt?: CommonDaoSaveOptions<DBM>): Promise<DBM>;
101
100
  saveAsDBM(dbm: DBM, opt?: CommonDaoSaveOptions<DBM>): Promise<DBM>;
102
- saveBatch(bms: BM[], opt?: CommonDaoSaveOptions<DBM>): Promise<Saved<BM>[]>;
101
+ saveBatch(bms: Unsaved<BM>[], opt?: CommonDaoSaveOptions<DBM>): Promise<Saved<BM>[]>;
103
102
  saveBatchAsDBM(dbms: DBM[], opt?: CommonDaoSaveOptions<DBM>): Promise<DBM[]>;
104
103
  /**
105
104
  * @returns number of deleted items
@@ -175,14 +175,25 @@ class CommonDao {
175
175
  /**
176
176
  * Throws if readOnly is true
177
177
  */
178
- requireObjectMutability() {
179
- if (this.cfg.immutable) {
178
+ requireObjectMutability(opt) {
179
+ if (this.cfg.immutable && !opt.allowMutability) {
180
180
  throw new js_lib_1.AppError(cnst_1.DBLibError.OBJECT_IS_IMMUTABLE, {
181
181
  code: cnst_1.DBLibError.OBJECT_IS_IMMUTABLE,
182
182
  table: this.cfg.table,
183
183
  });
184
184
  }
185
185
  }
186
+ async ensureUniqueId(table, dbm) {
187
+ // todo: retry N times
188
+ const existing = await this.cfg.db.getByIds(table, [dbm.id]);
189
+ if (existing.length) {
190
+ throw new js_lib_1.AppError(cnst_1.DBLibError.NON_UNIQUE_ID, {
191
+ table,
192
+ code: cnst_1.DBLibError.NON_UNIQUE_ID,
193
+ ids: existing.map(i => i.id),
194
+ });
195
+ }
196
+ }
186
197
  async getBy(by, value, limit = 0, opt) {
187
198
  return await this.query().filterEq(by, value).limit(limit).runQuery(opt);
188
199
  }
@@ -445,8 +456,9 @@ class CommonDao {
445
456
  const table = opt.table || this.cfg.table;
446
457
  if (opt.ensureUniqueId && idWasGenerated)
447
458
  await this.ensureUniqueId(table, dbm);
448
- if (this.cfg.immutable)
449
- await this.ensureImmutableDontExist(table, [dbm.id]);
459
+ if (this.cfg.immutable && !opt.allowMutability) {
460
+ opt.saveMethod || (opt.saveMethod = 'insert');
461
+ }
450
462
  const op = `save(${dbm.id})`;
451
463
  const started = this.logSaveStarted(op, bm, table);
452
464
  await this.cfg.db.saveBatch(table, [dbm], {
@@ -456,30 +468,6 @@ class CommonDao {
456
468
  this.logSaveResult(started, op, table);
457
469
  return bm;
458
470
  }
459
- async ensureImmutableDontExist(table, ids) {
460
- await this.throwIfObjectExists(table, ids, [
461
- cnst_1.DBLibError.OBJECT_IS_IMMUTABLE,
462
- {
463
- code: cnst_1.DBLibError.OBJECT_IS_IMMUTABLE,
464
- table,
465
- },
466
- ]);
467
- }
468
- async ensureUniqueId(table, dbm) {
469
- // todo: retry N times
470
- await this.throwIfObjectExists(table, [dbm.id], [
471
- cnst_1.DBLibError.OBJECT_IS_IMMUTABLE,
472
- {
473
- code: cnst_1.DBLibError.OBJECT_IS_IMMUTABLE,
474
- table,
475
- },
476
- ]);
477
- }
478
- async throwIfObjectExists(table, ids, errorMeta) {
479
- const existing = await this.cfg.db.getByIds(table, ids);
480
- if (existing.length > 0)
481
- throw new js_lib_1.AppError(errorMeta[0], { ...errorMeta[1], ids: existing.map(i => i.id) });
482
- }
483
471
  /**
484
472
  * Loads the row by id.
485
473
  * Creates the row (via this.create()) if it doesn't exist
@@ -513,8 +501,9 @@ class CommonDao {
513
501
  dbm = this.anyToDBM(dbm, opt);
514
502
  if (opt.ensureUniqueId && idWasGenerated)
515
503
  await this.ensureUniqueId(table, dbm);
516
- if (this.cfg.immutable)
517
- await this.ensureImmutableDontExist(table, [dbm.id]);
504
+ }
505
+ if (this.cfg.immutable && !opt.allowMutability) {
506
+ opt.saveMethod || (opt.saveMethod = 'insert');
518
507
  }
519
508
  const op = `saveAsDBM(${dbm.id})`;
520
509
  const started = this.logSaveStarted(op, dbm, table);
@@ -532,8 +521,9 @@ class CommonDao {
532
521
  const dbms = await this.bmsToDBM(bms, opt);
533
522
  if (opt.ensureUniqueId)
534
523
  throw new js_lib_1.AppError('ensureUniqueId is not supported in saveBatch');
535
- if (this.cfg.immutable)
536
- await this.ensureImmutableDontExist(table, dbms.map(dbm => dbm.id));
524
+ if (this.cfg.immutable && !opt.allowMutability) {
525
+ opt.saveMethod || (opt.saveMethod = 'insert');
526
+ }
537
527
  const op = `saveBatch ${dbms.length} row(s) (${(0, js_lib_1._truncate)(dbms
538
528
  .slice(0, 10)
539
529
  .map(bm => bm.id)
@@ -554,8 +544,9 @@ class CommonDao {
554
544
  dbms = this.anyToDBMs(dbms, opt);
555
545
  if (opt.ensureUniqueId)
556
546
  throw new js_lib_1.AppError('ensureUniqueId is not supported in saveBatch');
557
- if (this.cfg.immutable)
558
- await this.ensureImmutableDontExist(table, dbms.map(dbm => dbm.id));
547
+ }
548
+ if (this.cfg.immutable && !opt.allowMutability) {
549
+ opt.saveMethod || (opt.saveMethod = 'insert');
559
550
  }
560
551
  const op = `saveBatchAsDBM ${dbms.length} row(s) (${(0, js_lib_1._truncate)(dbms
561
552
  .slice(0, 10)
@@ -573,8 +564,7 @@ class CommonDao {
573
564
  if (!id)
574
565
  return 0;
575
566
  this.requireWriteAccess();
576
- if (!opt.allowMutability)
577
- this.requireObjectMutability();
567
+ this.requireObjectMutability(opt);
578
568
  const op = `deleteById(${id})`;
579
569
  const table = opt.table || this.cfg.table;
580
570
  const started = this.logStarted(op, table);
@@ -584,8 +574,7 @@ class CommonDao {
584
574
  }
585
575
  async deleteByIds(ids, opt = {}) {
586
576
  this.requireWriteAccess();
587
- if (!opt.allowMutability)
588
- this.requireObjectMutability();
577
+ this.requireObjectMutability(opt);
589
578
  const op = `deleteByIds(${ids.join(', ')})`;
590
579
  const table = opt.table || this.cfg.table;
591
580
  const started = this.logStarted(op, table);
@@ -600,8 +589,7 @@ class CommonDao {
600
589
  */
601
590
  async deleteByQuery(q, opt = {}) {
602
591
  this.requireWriteAccess();
603
- if (!opt.allowMutability)
604
- this.requireObjectMutability();
592
+ this.requireObjectMutability(opt);
605
593
  q.table = opt.table || q.table;
606
594
  const op = `deleteByQuery(${q.pretty()})`;
607
595
  const started = this.logStarted(op, q.table);
@@ -49,17 +49,18 @@ export interface CommonDaoCfg<BM extends Partial<ObjectWithId<ID>>, DBM extends
49
49
  tmSchema?: ObjectSchemaTyped<TM> | AjvSchema<TM>;
50
50
  excludeFromIndexes?: (keyof DBM)[];
51
51
  /**
52
- * @default to false
53
- * Set to true to limit DB writing:
54
- * * Will throw an error if an object with matching ID already exists during save().
55
- * * saveBatch, delete*() and patch() will throw.
52
+ * Defaults to false.
53
+ * Setting it to true will set saveMethod to `insert` for save/saveBatch, which will
54
+ * fail for rows that already exist in the DB (if CommonDB implementation supports it).
56
55
  *
57
- * Although deletion is possible by passing (opt.overrideImmutability === true)
56
+ * `delete*` and `patch` will throw.
57
+ *
58
+ * You can still override saveMethod, or set opt.allowMutability to allow deletion.
58
59
  */
59
60
  immutable?: boolean;
60
61
  /**
61
- * @default to false
62
- * Set to true to limit DB writing (will throw an error is such case).
62
+ * Defaults to false.
63
+ * Set to true to limit DB writing (will throw an error in such case).
63
64
  */
64
65
  readOnly?: boolean;
65
66
  /**
@@ -165,7 +166,7 @@ export interface CommonDaoOptions extends CommonDBOptions {
165
166
  /**
166
167
  * All properties default to undefined.
167
168
  */
168
- export interface CommonDaoSaveOptions<DBM extends ObjectWithId> extends CommonDaoOptions, CommonDBSaveOptions<DBM> {
169
+ export interface CommonDaoSaveOptions<DBM extends Partial<ObjectWithId>> extends CommonDaoOptions, CommonDBSaveOptions<DBM> {
169
170
  /**
170
171
  * @default false
171
172
  *
@@ -1,11 +1,24 @@
1
1
  import { AnyObjectWithId, ObjectWithId } from '@naturalcycles/js-lib';
2
+ /**
3
+ * Similar to SQL INSERT, UPDATE.
4
+ * Insert will fail if row already exists.
5
+ * Update will fail if row is missing.
6
+ * Upsert will auto-detect and use Insert or Update to not fail.
7
+ *
8
+ * Default is Upsert.
9
+ */
10
+ export declare type CommonDBSaveMethod = 'upsert' | 'insert' | 'update';
2
11
  export interface CommonDBOptions {
3
12
  }
4
13
  /**
5
14
  * All properties default to undefined.
6
15
  */
7
- export interface CommonDBSaveOptions<ROW extends ObjectWithId = AnyObjectWithId> extends CommonDBOptions {
16
+ export interface CommonDBSaveOptions<ROW extends Partial<ObjectWithId> = AnyObjectWithId> extends CommonDBOptions {
8
17
  excludeFromIndexes?: (keyof ROW)[];
18
+ /**
19
+ * Default is `upsert`
20
+ */
21
+ saveMethod?: CommonDBSaveMethod;
9
22
  }
10
23
  export declare type CommonDBStreamOptions = CommonDBOptions;
11
24
  export interface CommonDBCreateOptions extends CommonDBOptions {
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ import { DBLibError } from './cnst';
6
6
  import { CommonDB } from './common.db';
7
7
  import { CommonDao } from './commondao/common.dao';
8
8
  import { CommonDaoCfg, CommonDaoCreateOptions, CommonDaoLogLevel, CommonDaoOptions, CommonDaoSaveOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions, CommonDaoHooks } from './commondao/common.dao.model';
9
- import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, DBDeleteByIdsOperation, DBModelType, DBOperation, DBRelation, DBSaveBatchOperation, RunQueryResult } from './db.model';
9
+ import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveMethod, CommonDBSaveOptions, CommonDBStreamOptions, DBDeleteByIdsOperation, DBModelType, DBOperation, DBRelation, DBSaveBatchOperation, RunQueryResult } from './db.model';
10
10
  import { CommonKeyValueDao, CommonKeyValueDaoCfg } from './kv/commonKeyValueDao';
11
11
  import { CommonKeyValueDB, KeyValueDBTuple } from './kv/commonKeyValueDB';
12
12
  import { createdUpdatedFields, createdUpdatedIdFields, deserializeJsonField, serializeJsonField } from './model.util';
@@ -17,5 +17,5 @@ import { DBQuery, DBQueryFilter, DBQueryFilterOperator, dbQueryFilterOperatorVal
17
17
  import { DBTransaction, RunnableDBTransaction } from './transaction/dbTransaction';
18
18
  import { commitDBTransactionSimple, mergeDBOperations } from './transaction/dbTransaction.util';
19
19
  export * from './kv/commonKeyValueDaoMemoCache';
20
- export type { DBQueryFilterOperator, DBQueryFilter, DBQueryOrder, CommonDaoCreateOptions, CommonDaoOptions, CommonDaoSaveOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions, CommonDaoHooks, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, CommonDBCreateOptions, CommonDB, RunQueryResult, CommonDaoCfg, InMemoryDBCfg, InMemoryKeyValueDBCfg, DBPipelineBackupOptions, DBPipelineRestoreOptions, DBPipelineCopyOptions, DBOperation, DBSaveBatchOperation, DBDeleteByIdsOperation, CommonKeyValueDB, CommonKeyValueDaoCfg, KeyValueDBTuple, };
20
+ export type { DBQueryFilterOperator, DBQueryFilter, DBQueryOrder, CommonDaoCreateOptions, CommonDaoOptions, CommonDaoSaveOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions, CommonDaoHooks, CommonDBOptions, CommonDBSaveOptions, CommonDBSaveMethod, CommonDBStreamOptions, CommonDBCreateOptions, CommonDB, RunQueryResult, CommonDaoCfg, InMemoryDBCfg, InMemoryKeyValueDBCfg, DBPipelineBackupOptions, DBPipelineRestoreOptions, DBPipelineCopyOptions, DBOperation, DBSaveBatchOperation, DBDeleteByIdsOperation, CommonKeyValueDB, CommonKeyValueDaoCfg, KeyValueDBTuple, };
21
21
  export { DBQuery, dbQueryFilterOperatorValues, RunnableDBQuery, CommonDaoLogLevel, DBRelation, DBModelType, CommonDao, createdUpdatedFields, createdUpdatedIdFields, InMemoryDB, InMemoryKeyValueDB, queryInMemory, serializeJsonField, deserializeJsonField, dbPipelineBackup, dbPipelineRestore, dbPipelineCopy, DBLibError, BaseCommonDB, DBTransaction, RunnableDBTransaction, mergeDBOperations, commitDBTransactionSimple, CommonKeyValueDao, };
@@ -7,7 +7,6 @@ const __1 = require("..");
7
7
  const common_dao_1 = require("../commondao/common.dao");
8
8
  const dbTest_1 = require("./dbTest");
9
9
  const test_model_1 = require("./test.model");
10
- const _1 = require(".");
11
10
  function runCommonDaoTest(db, features = {}, quirks = {}) {
12
11
  const dao = new common_dao_1.CommonDao({
13
12
  table: test_model_1.TEST_TABLE,
@@ -42,7 +41,7 @@ function runCommonDaoTest(db, features = {}, quirks = {}) {
42
41
  // CREATE TABLE, DROP
43
42
  if (createTable) {
44
43
  test('createTable, dropIfExists=true', async () => {
45
- await dao.createTable((0, _1.getTestItemSchema)(), { dropIfExists: true });
44
+ await dao.createTable(test_model_1.testItemDBMJsonSchema.build(), { dropIfExists: true });
46
45
  });
47
46
  }
48
47
  if (querying) {
@@ -8,6 +8,8 @@ export interface CommonDBImplementationFeatures {
8
8
  dbQueryFilterIn?: boolean;
9
9
  dbQueryOrder?: boolean;
10
10
  dbQuerySelectFields?: boolean;
11
+ insert?: boolean;
12
+ update?: boolean;
11
13
  createTable?: boolean;
12
14
  tableSchemas?: boolean;
13
15
  /**
@@ -12,7 +12,7 @@ const test_util_1 = require("./test.util");
12
12
  function runCommonDBTest(db, features = {}, quirks = {}) {
13
13
  const { querying = true, tableSchemas = true, createTable = true, dbQueryFilter = true,
14
14
  // dbQueryFilterIn = true,
15
- dbQueryOrder = true, dbQuerySelectFields = true, streaming = true, strongConsistency = true, bufferSupport = true, nullValues = true, documentDB = true, } = features;
15
+ dbQueryOrder = true, dbQuerySelectFields = true, insert = true, update = true, streaming = true, strongConsistency = true, bufferSupport = true, nullValues = true, documentDB = true, } = features;
16
16
  // const {
17
17
  // allowExtraPropertiesInResponse,
18
18
  // allowBooleansAsUndefined,
@@ -28,7 +28,7 @@ function runCommonDBTest(db, features = {}, quirks = {}) {
28
28
  // CREATE TABLE, DROP
29
29
  if (createTable) {
30
30
  test('createTable, dropIfExists=true', async () => {
31
- await db.createTable(test_model_1.TEST_TABLE, (0, test_model_1.getTestItemSchema)(), { dropIfExists: true });
31
+ await db.createTable(test_model_1.TEST_TABLE, test_model_1.testItemDBMJsonSchema.build(), { dropIfExists: true });
32
32
  });
33
33
  }
34
34
  if (querying) {
@@ -87,9 +87,24 @@ function runCommonDBTest(db, features = {}, quirks = {}) {
87
87
  expect(Object.keys(item3Loaded)).not.toContain('k2');
88
88
  });
89
89
  }
90
+ if (update) {
91
+ test('saveBatch UPDATE method should throw', async () => {
92
+ await expect(db.saveBatch(test_model_1.TEST_TABLE, items, { saveMethod: 'update' })).rejects.toThrow();
93
+ });
94
+ }
90
95
  test('saveBatch test items', async () => {
91
96
  await db.saveBatch(test_model_1.TEST_TABLE, items);
92
97
  });
98
+ if (insert) {
99
+ test('saveBatch INSERT method should throw', async () => {
100
+ await expect(db.saveBatch(test_model_1.TEST_TABLE, items, { saveMethod: 'insert' })).rejects.toThrow();
101
+ });
102
+ }
103
+ if (update) {
104
+ test('saveBatch UPDATE method should pass', async () => {
105
+ await db.saveBatch(test_model_1.TEST_TABLE, items, { saveMethod: 'update' });
106
+ });
107
+ }
93
108
  // GET not empty
94
109
  test('getByIds all items', async () => {
95
110
  const rows = await db.getByIds(test_model_1.TEST_TABLE, items.map(i => i.id).concat('abcd'));
@@ -1,6 +1,6 @@
1
1
  import { runCommonDaoTest } from './daoTest';
2
2
  import { CommonDBImplementationFeatures, CommonDBImplementationQuirks, runCommonDBTest } from './dbTest';
3
3
  import { runCommonKeyValueDBTest } from './keyValueDBTest';
4
- import { createTestItemBM, createTestItemDBM, createTestItemsBM, createTestItemsDBM, getTestItemSchema, TestItemBM, testItemBMSchema, TestItemDBM, testItemDBMSchema, TestItemTM, testItemTMSchema, TEST_TABLE } from './test.model';
4
+ import { createTestItemBM, createTestItemDBM, createTestItemsBM, createTestItemsDBM, TestItemBM, testItemBMJsonSchema, testItemBMSchema, TestItemDBM, testItemDBMJsonSchema, testItemDBMSchema, TestItemTM, testItemTMSchema, TEST_TABLE } from './test.model';
5
5
  export type { TestItemDBM, TestItemBM, TestItemTM, CommonDBImplementationFeatures, CommonDBImplementationQuirks, };
6
- export { TEST_TABLE, createTestItemDBM, createTestItemBM, createTestItemsDBM, createTestItemsBM, testItemDBMSchema, testItemBMSchema, testItemTMSchema, getTestItemSchema, runCommonDBTest, runCommonDaoTest, runCommonKeyValueDBTest, };
6
+ export { TEST_TABLE, createTestItemDBM, createTestItemBM, createTestItemsDBM, createTestItemsBM, testItemDBMSchema, testItemBMSchema, testItemTMSchema, testItemBMJsonSchema, testItemDBMJsonSchema, runCommonDBTest, runCommonDaoTest, runCommonKeyValueDBTest, };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runCommonKeyValueDBTest = exports.runCommonDaoTest = exports.runCommonDBTest = exports.getTestItemSchema = exports.testItemTMSchema = exports.testItemBMSchema = exports.testItemDBMSchema = exports.createTestItemsBM = exports.createTestItemsDBM = exports.createTestItemBM = exports.createTestItemDBM = exports.TEST_TABLE = void 0;
3
+ exports.runCommonKeyValueDBTest = exports.runCommonDaoTest = exports.runCommonDBTest = exports.testItemDBMJsonSchema = exports.testItemBMJsonSchema = exports.testItemTMSchema = exports.testItemBMSchema = exports.testItemDBMSchema = exports.createTestItemsBM = exports.createTestItemsDBM = exports.createTestItemBM = exports.createTestItemDBM = exports.TEST_TABLE = void 0;
4
4
  const daoTest_1 = require("./daoTest");
5
5
  Object.defineProperty(exports, "runCommonDaoTest", { enumerable: true, get: function () { return daoTest_1.runCommonDaoTest; } });
6
6
  const dbTest_1 = require("./dbTest");
@@ -12,8 +12,9 @@ Object.defineProperty(exports, "createTestItemBM", { enumerable: true, get: func
12
12
  Object.defineProperty(exports, "createTestItemDBM", { enumerable: true, get: function () { return test_model_1.createTestItemDBM; } });
13
13
  Object.defineProperty(exports, "createTestItemsBM", { enumerable: true, get: function () { return test_model_1.createTestItemsBM; } });
14
14
  Object.defineProperty(exports, "createTestItemsDBM", { enumerable: true, get: function () { return test_model_1.createTestItemsDBM; } });
15
- Object.defineProperty(exports, "getTestItemSchema", { enumerable: true, get: function () { return test_model_1.getTestItemSchema; } });
15
+ Object.defineProperty(exports, "testItemBMJsonSchema", { enumerable: true, get: function () { return test_model_1.testItemBMJsonSchema; } });
16
16
  Object.defineProperty(exports, "testItemBMSchema", { enumerable: true, get: function () { return test_model_1.testItemBMSchema; } });
17
+ Object.defineProperty(exports, "testItemDBMJsonSchema", { enumerable: true, get: function () { return test_model_1.testItemDBMJsonSchema; } });
17
18
  Object.defineProperty(exports, "testItemDBMSchema", { enumerable: true, get: function () { return test_model_1.testItemDBMSchema; } });
18
19
  Object.defineProperty(exports, "testItemTMSchema", { enumerable: true, get: function () { return test_model_1.testItemTMSchema; } });
19
20
  Object.defineProperty(exports, "TEST_TABLE", { enumerable: true, get: function () { return test_model_1.TEST_TABLE; } });
@@ -1,5 +1,5 @@
1
1
  /// <reference types="node" />
2
- import { JsonSchemaObject, BaseDBEntity, Saved } from '@naturalcycles/js-lib';
2
+ import { BaseDBEntity, Saved } from '@naturalcycles/js-lib';
3
3
  export declare const TEST_TABLE = "TEST_TABLE";
4
4
  export interface TestItemBM extends BaseDBEntity {
5
5
  k1: string;
@@ -23,4 +23,3 @@ export declare function createTestItemDBM(num?: number): TestItemDBM;
23
23
  export declare function createTestItemBM(num?: number): Saved<TestItemBM>;
24
24
  export declare function createTestItemsDBM(count?: number): TestItemDBM[];
25
25
  export declare function createTestItemsBM(count?: number): Saved<TestItemBM>[];
26
- export declare function getTestItemSchema(): JsonSchemaObject<TestItemDBM>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getTestItemSchema = exports.createTestItemsBM = exports.createTestItemsDBM = exports.createTestItemBM = exports.createTestItemDBM = exports.testItemDBMJsonSchema = exports.testItemBMJsonSchema = exports.testItemTMSchema = exports.testItemDBMSchema = exports.testItemBMSchema = exports.TEST_TABLE = void 0;
3
+ exports.createTestItemsBM = exports.createTestItemsDBM = exports.createTestItemBM = exports.createTestItemDBM = exports.testItemDBMJsonSchema = exports.testItemBMJsonSchema = exports.testItemTMSchema = exports.testItemDBMSchema = exports.testItemBMSchema = exports.TEST_TABLE = void 0;
4
4
  const js_lib_1 = require("@naturalcycles/js-lib");
5
5
  const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
6
6
  const MOCK_TS_2018_06_21 = 1529539200;
@@ -67,19 +67,3 @@ function createTestItemsBM(count = 1) {
67
67
  return (0, js_lib_1._range)(1, count + 1).map(num => createTestItemBM(num));
68
68
  }
69
69
  exports.createTestItemsBM = createTestItemsBM;
70
- const testItemJsonSchema = js_lib_1.jsonSchema
71
- .object({
72
- id: js_lib_1.jsonSchema.string(),
73
- k1: js_lib_1.jsonSchema.string(),
74
- k2: js_lib_1.jsonSchema.string(),
75
- k3: js_lib_1.jsonSchema.number(),
76
- even: js_lib_1.jsonSchema.boolean(),
77
- created: js_lib_1.jsonSchema.unixTimestamp(),
78
- updated: js_lib_1.jsonSchema.unixTimestamp(),
79
- })
80
- .build();
81
- function getTestItemSchema() {
82
- // return CommonSchemaGenerator.generateFromRows({ table: TEST_TABLE }, createTestItemsDBM())
83
- return testItemJsonSchema;
84
- }
85
- exports.getTestItemSchema = getTestItemSchema;
package/package.json CHANGED
@@ -42,7 +42,7 @@
42
42
  "engines": {
43
43
  "node": ">=14.15"
44
44
  },
45
- "version": "8.36.1",
45
+ "version": "8.38.0",
46
46
  "description": "Lowest Common Denominator API to supported Databases",
47
47
  "keywords": [
48
48
  "db",
@@ -57,7 +57,7 @@ export interface CacheDBOptions {
57
57
  onlyCache?: boolean
58
58
  }
59
59
 
60
- export interface CacheDBSaveOptions<ROW extends ObjectWithId>
60
+ export interface CacheDBSaveOptions<ROW extends Partial<ObjectWithId>>
61
61
  extends CacheDBOptions,
62
62
  CommonDBSaveOptions<ROW> {}
63
63
 
@@ -146,7 +146,7 @@ export class CacheDB extends BaseCommonDB implements CommonDB {
146
146
  return deletedIds
147
147
  }
148
148
 
149
- override async saveBatch<ROW extends ObjectWithId>(
149
+ override async saveBatch<ROW extends Partial<ObjectWithId>>(
150
150
  table: string,
151
151
  rows: ROW[],
152
152
  opt: CacheDBSaveOptions<ROW> = {},
@@ -13,6 +13,7 @@ import {
13
13
  _filterUndefinedValues,
14
14
  ObjectWithId,
15
15
  AnyObjectWithId,
16
+ _assert,
16
17
  } from '@naturalcycles/js-lib'
17
18
  import { readableCreate, ReadableTyped } from '@naturalcycles/nodejs-lib'
18
19
  import { dimGrey } from '@naturalcycles/nodejs-lib/dist/colors'
@@ -71,7 +72,7 @@ export class FileDB extends BaseCommonDB implements CommonDB {
71
72
  return ids.map(id => byId[id]!).filter(Boolean)
72
73
  }
73
74
 
74
- override async saveBatch<ROW extends ObjectWithId>(
75
+ override async saveBatch<ROW extends Partial<ObjectWithId>>(
75
76
  table: string,
76
77
  rows: ROW[],
77
78
  _opt?: CommonDBSaveOptions<ROW>,
@@ -79,13 +80,15 @@ export class FileDB extends BaseCommonDB implements CommonDB {
79
80
  if (!rows.length) return // save some api calls
80
81
 
81
82
  // 1. Load the whole file
82
- const byId = _by(await this.loadFile<ROW>(table), r => r.id)
83
+ const byId = _by(await this.loadFile<ROW & ObjectWithId>(table), r => r.id)
83
84
 
84
85
  // 2. Merge with new data (using ids)
85
86
  let saved = 0
86
87
  rows.forEach(r => {
88
+ _assert(r.id, 'FileDB: row.id is required')
89
+
87
90
  if (!_deepEquals(byId[r.id], r)) {
88
- byId[r.id] = r
91
+ byId[r.id] = r as any
89
92
  saved++
90
93
  }
91
94
  })
@@ -151,10 +151,10 @@ export class InMemoryDB implements CommonDB {
151
151
  return ids.map(id => this.data[table]![id]).filter(Boolean) as ROW[]
152
152
  }
153
153
 
154
- async saveBatch<ROW extends ObjectWithId>(
154
+ async saveBatch<ROW extends Partial<ObjectWithId>>(
155
155
  _table: string,
156
156
  rows: ROW[],
157
- _opt?: CommonDBSaveOptions<ROW>,
157
+ opt: CommonDBSaveOptions<ROW> = {},
158
158
  ): Promise<void> {
159
159
  const table = this.cfg.tablesPrefix + _table
160
160
  this.data[table] ||= {}
@@ -162,20 +162,23 @@ export class InMemoryDB implements CommonDB {
162
162
  rows.forEach(r => {
163
163
  if (!r.id) {
164
164
  this.cfg.logger?.warn({ rows })
165
- throw new Error(`InMemoryDB: id doesn't exist for row`)
165
+ throw new Error(
166
+ `InMemoryDB doesn't support id auto-generation in saveBatch, row without id was given`,
167
+ )
168
+ }
169
+
170
+ if (opt.saveMethod === 'insert' && this.data[table]![r.id]) {
171
+ throw new Error(`InMemoryDB: INSERT failed, entity exists: ${table}.${r.id}`)
172
+ }
173
+
174
+ if (opt.saveMethod === 'update' && !this.data[table]![r.id]) {
175
+ throw new Error(`InMemoryDB: UPDATE failed, entity doesn't exist: ${table}.${r.id}`)
166
176
  }
167
177
 
168
178
  // JSON parse/stringify (deep clone) is to:
169
179
  // 1. Not store values "by reference" (avoid mutation bugs)
170
180
  // 2. Simulate real DB that would do something like that in a transport layer anyway
171
181
  this.data[table]![r.id] = JSON.parse(JSON.stringify(r), bufferReviver)
172
-
173
- // special treatment for Buffers (assign them raw, without JSON parse/stringify)
174
- // Object.entries(r).forEach(([k, v]) => {
175
- // if (Buffer.isBuffer(v)) {
176
- // this.data[table]![r.id]![k] = v
177
- // }
178
- // })
179
182
  })
180
183
  }
181
184
 
@@ -55,7 +55,7 @@ export class BaseCommonDB implements CommonDB {
55
55
  return 0
56
56
  }
57
57
 
58
- async saveBatch<ROW extends ObjectWithId>(
58
+ async saveBatch<ROW extends Partial<ObjectWithId>>(
59
59
  _table: string,
60
60
  _rows: ROW[],
61
61
  _opt?: CommonDBSaveOptions<ROW>,
package/src/common.db.ts CHANGED
@@ -70,7 +70,10 @@ export interface CommonDB {
70
70
  ): ReadableTyped<ROW>
71
71
 
72
72
  // SAVE
73
- saveBatch<ROW extends ObjectWithId>(
73
+ /**
74
+ * rows can have missing ids only if DB supports auto-generating them (like mysql auto_increment).
75
+ */
76
+ saveBatch<ROW extends Partial<ObjectWithId>>(
74
77
  table: string,
75
78
  rows: ROW[],
76
79
  opt?: CommonDBSaveOptions<ROW>,
@@ -73,18 +73,19 @@ export interface CommonDaoCfg<
73
73
  excludeFromIndexes?: (keyof DBM)[]
74
74
 
75
75
  /**
76
- * @default to false
77
- * Set to true to limit DB writing:
78
- * * Will throw an error if an object with matching ID already exists during save().
79
- * * saveBatch, delete*() and patch() will throw.
76
+ * Defaults to false.
77
+ * Setting it to true will set saveMethod to `insert` for save/saveBatch, which will
78
+ * fail for rows that already exist in the DB (if CommonDB implementation supports it).
80
79
  *
81
- * Although deletion is possible by passing (opt.overrideImmutability === true)
80
+ * `delete*` and `patch` will throw.
81
+ *
82
+ * You can still override saveMethod, or set opt.allowMutability to allow deletion.
82
83
  */
83
84
  immutable?: boolean
84
85
 
85
86
  /**
86
- * @default to false
87
- * Set to true to limit DB writing (will throw an error is such case).
87
+ * Defaults to false.
88
+ * Set to true to limit DB writing (will throw an error in such case).
88
89
  */
89
90
  readOnly?: boolean
90
91
 
@@ -209,7 +210,7 @@ export interface CommonDaoOptions extends CommonDBOptions {
209
210
  /**
210
211
  * All properties default to undefined.
211
212
  */
212
- export interface CommonDaoSaveOptions<DBM extends ObjectWithId>
213
+ export interface CommonDaoSaveOptions<DBM extends Partial<ObjectWithId>>
213
214
  extends CommonDaoOptions,
214
215
  CommonDBSaveOptions<DBM> {
215
216
  /**
@@ -15,6 +15,7 @@ import {
15
15
  pMap,
16
16
  pTimeout,
17
17
  Saved,
18
+ Unsaved,
18
19
  } from '@naturalcycles/js-lib'
19
20
  import {
20
21
  _pipeline,
@@ -244,8 +245,8 @@ export class CommonDao<
244
245
  /**
245
246
  * Throws if readOnly is true
246
247
  */
247
- private requireObjectMutability(): void {
248
- if (this.cfg.immutable) {
248
+ private requireObjectMutability(opt: CommonDaoOptions): void {
249
+ if (this.cfg.immutable && !opt.allowMutability) {
249
250
  throw new AppError(DBLibError.OBJECT_IS_IMMUTABLE, {
250
251
  code: DBLibError.OBJECT_IS_IMMUTABLE,
251
252
  table: this.cfg.table,
@@ -253,6 +254,18 @@ export class CommonDao<
253
254
  }
254
255
  }
255
256
 
257
+ private async ensureUniqueId(table: string, dbm: DBM): Promise<void> {
258
+ // todo: retry N times
259
+ const existing = await this.cfg.db.getByIds<DBM>(table, [dbm.id])
260
+ if (existing.length) {
261
+ throw new AppError(DBLibError.NON_UNIQUE_ID, {
262
+ table,
263
+ code: DBLibError.NON_UNIQUE_ID,
264
+ ids: existing.map(i => i.id),
265
+ })
266
+ }
267
+ }
268
+
256
269
  async getBy(by: keyof DBM, value: any, limit = 0, opt?: CommonDaoOptions): Promise<Saved<BM>[]> {
257
270
  return await this.query().filterEq(by, value).limit(limit).runQuery(opt)
258
271
  }
@@ -573,10 +586,11 @@ export class CommonDao<
573
586
  */
574
587
  assignIdCreatedUpdated(obj: DBM, opt?: CommonDaoOptions): DBM
575
588
  assignIdCreatedUpdated(obj: BM, opt?: CommonDaoOptions): Saved<BM>
576
- assignIdCreatedUpdated(obj: DBM | BM, opt: CommonDaoOptions = {}): DBM | Saved<BM> {
589
+ assignIdCreatedUpdated(obj: Unsaved<BM>, opt?: CommonDaoOptions): Saved<BM>
590
+ assignIdCreatedUpdated(obj: DBM | BM | Unsaved<BM>, opt: CommonDaoOptions = {}): DBM | Saved<BM> {
577
591
  const now = Math.floor(Date.now() / 1000)
578
592
 
579
- obj.id ||= this.cfg.hooks!.createId?.(obj)
593
+ obj.id ||= this.cfg.hooks!.createId?.(obj as BM)
580
594
 
581
595
  if (this.cfg.created) {
582
596
  obj['created'] ||= obj['updated'] || now
@@ -593,14 +607,16 @@ export class CommonDao<
593
607
  /**
594
608
  * Mutates with id, created, updated
595
609
  */
596
- async save(bm: BM, opt: CommonDaoSaveOptions<DBM> = {}): Promise<Saved<BM>> {
610
+ async save(bm: Unsaved<BM>, opt: CommonDaoSaveOptions<DBM> = {}): Promise<Saved<BM>> {
597
611
  this.requireWriteAccess()
598
612
  const idWasGenerated = !bm.id
599
613
  this.assignIdCreatedUpdated(bm, opt) // mutates
600
- const dbm = await this.bmToDBM(bm, opt)
614
+ const dbm = await this.bmToDBM(bm as BM, opt)
601
615
  const table = opt.table || this.cfg.table
602
616
  if (opt.ensureUniqueId && idWasGenerated) await this.ensureUniqueId(table, dbm)
603
- if (this.cfg.immutable) await this.ensureImmutableDontExist(table, [dbm.id])
617
+ if (this.cfg.immutable && !opt.allowMutability) {
618
+ opt.saveMethod ||= 'insert'
619
+ }
604
620
  const op = `save(${dbm.id})`
605
621
  const started = this.logSaveStarted(op, bm, table)
606
622
  await this.cfg.db.saveBatch(table, [dbm], {
@@ -612,41 +628,6 @@ export class CommonDao<
612
628
  return bm as any
613
629
  }
614
630
 
615
- private async ensureImmutableDontExist(table: string, ids: ID[]): Promise<void> {
616
- await this.throwIfObjectExists(table, ids, [
617
- DBLibError.OBJECT_IS_IMMUTABLE,
618
- {
619
- code: DBLibError.OBJECT_IS_IMMUTABLE,
620
- table,
621
- },
622
- ])
623
- }
624
-
625
- private async ensureUniqueId(table: string, dbm: DBM): Promise<void> {
626
- // todo: retry N times
627
- await this.throwIfObjectExists(
628
- table,
629
- [dbm.id],
630
- [
631
- DBLibError.OBJECT_IS_IMMUTABLE,
632
- {
633
- code: DBLibError.OBJECT_IS_IMMUTABLE,
634
- table,
635
- },
636
- ],
637
- )
638
- }
639
-
640
- private async throwIfObjectExists(
641
- table: string,
642
- ids: ID[],
643
- errorMeta: [DBLibError, any],
644
- ): Promise<void> {
645
- const existing = await this.cfg.db.getByIds<DBM>(table, ids)
646
- if (existing.length > 0)
647
- throw new AppError(errorMeta[0], { ...errorMeta[1], ids: existing.map(i => i.id) })
648
- }
649
-
650
631
  /**
651
632
  * Loads the row by id.
652
633
  * Creates the row (via this.create()) if it doesn't exist
@@ -690,7 +671,9 @@ export class CommonDao<
690
671
  this.assignIdCreatedUpdated(dbm, opt) // mutates
691
672
  dbm = this.anyToDBM(dbm, opt)
692
673
  if (opt.ensureUniqueId && idWasGenerated) await this.ensureUniqueId(table, dbm)
693
- if (this.cfg.immutable) await this.ensureImmutableDontExist(table, [dbm.id])
674
+ }
675
+ if (this.cfg.immutable && !opt.allowMutability) {
676
+ opt.saveMethod ||= 'insert'
694
677
  }
695
678
  const op = `saveAsDBM(${dbm.id})`
696
679
  const started = this.logSaveStarted(op, dbm, table)
@@ -702,17 +685,15 @@ export class CommonDao<
702
685
  return dbm
703
686
  }
704
687
 
705
- async saveBatch(bms: BM[], opt: CommonDaoSaveOptions<DBM> = {}): Promise<Saved<BM>[]> {
688
+ async saveBatch(bms: Unsaved<BM>[], opt: CommonDaoSaveOptions<DBM> = {}): Promise<Saved<BM>[]> {
706
689
  this.requireWriteAccess()
707
690
  const table = opt.table || this.cfg.table
708
691
  bms.forEach(bm => this.assignIdCreatedUpdated(bm, opt))
709
- const dbms = await this.bmsToDBM(bms, opt)
692
+ const dbms = await this.bmsToDBM(bms as BM[], opt)
710
693
  if (opt.ensureUniqueId) throw new AppError('ensureUniqueId is not supported in saveBatch')
711
- if (this.cfg.immutable)
712
- await this.ensureImmutableDontExist(
713
- table,
714
- dbms.map(dbm => dbm.id),
715
- )
694
+ if (this.cfg.immutable && !opt.allowMutability) {
695
+ opt.saveMethod ||= 'insert'
696
+ }
716
697
 
717
698
  const op = `saveBatch ${dbms.length} row(s) (${_truncate(
718
699
  dbms
@@ -740,11 +721,9 @@ export class CommonDao<
740
721
  dbms.forEach(dbm => this.assignIdCreatedUpdated(dbm, opt)) // mutates
741
722
  dbms = this.anyToDBMs(dbms, opt)
742
723
  if (opt.ensureUniqueId) throw new AppError('ensureUniqueId is not supported in saveBatch')
743
- if (this.cfg.immutable)
744
- await this.ensureImmutableDontExist(
745
- table,
746
- dbms.map(dbm => dbm.id),
747
- )
724
+ }
725
+ if (this.cfg.immutable && !opt.allowMutability) {
726
+ opt.saveMethod ||= 'insert'
748
727
  }
749
728
  const op = `saveBatchAsDBM ${dbms.length} row(s) (${_truncate(
750
729
  dbms
@@ -773,7 +752,7 @@ export class CommonDao<
773
752
  async deleteById(id?: ID, opt: CommonDaoOptions = {}): Promise<number> {
774
753
  if (!id) return 0
775
754
  this.requireWriteAccess()
776
- if (!opt.allowMutability) this.requireObjectMutability()
755
+ this.requireObjectMutability(opt)
777
756
  const op = `deleteById(${id})`
778
757
  const table = opt.table || this.cfg.table
779
758
  const started = this.logStarted(op, table)
@@ -784,7 +763,7 @@ export class CommonDao<
784
763
 
785
764
  async deleteByIds(ids: ID[], opt: CommonDaoOptions = {}): Promise<number> {
786
765
  this.requireWriteAccess()
787
- if (!opt.allowMutability) this.requireObjectMutability()
766
+ this.requireObjectMutability(opt)
788
767
  const op = `deleteByIds(${ids.join(', ')})`
789
768
  const table = opt.table || this.cfg.table
790
769
  const started = this.logStarted(op, table)
@@ -803,7 +782,7 @@ export class CommonDao<
803
782
  opt: CommonDaoStreamForEachOptions<DBM> & { stream?: boolean } = {},
804
783
  ): Promise<number> {
805
784
  this.requireWriteAccess()
806
- if (!opt.allowMutability) this.requireObjectMutability()
785
+ this.requireObjectMutability(opt)
807
786
  q.table = opt.table || q.table
808
787
  const op = `deleteByQuery(${q.pretty()})`
809
788
  const started = this.logStarted(op, q.table)
package/src/db.model.ts CHANGED
@@ -1,14 +1,29 @@
1
1
  import { AnyObjectWithId, ObjectWithId } from '@naturalcycles/js-lib'
2
2
 
3
+ /**
4
+ * Similar to SQL INSERT, UPDATE.
5
+ * Insert will fail if row already exists.
6
+ * Update will fail if row is missing.
7
+ * Upsert will auto-detect and use Insert or Update to not fail.
8
+ *
9
+ * Default is Upsert.
10
+ */
11
+ export type CommonDBSaveMethod = 'upsert' | 'insert' | 'update'
12
+
3
13
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
4
14
  export interface CommonDBOptions {}
5
15
 
6
16
  /**
7
17
  * All properties default to undefined.
8
18
  */
9
- export interface CommonDBSaveOptions<ROW extends ObjectWithId = AnyObjectWithId>
19
+ export interface CommonDBSaveOptions<ROW extends Partial<ObjectWithId> = AnyObjectWithId>
10
20
  extends CommonDBOptions {
11
21
  excludeFromIndexes?: (keyof ROW)[]
22
+
23
+ /**
24
+ * Default is `upsert`
25
+ */
26
+ saveMethod?: CommonDBSaveMethod
12
27
  }
13
28
 
14
29
  export type CommonDBStreamOptions = CommonDBOptions
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  import {
19
19
  CommonDBCreateOptions,
20
20
  CommonDBOptions,
21
+ CommonDBSaveMethod,
21
22
  CommonDBSaveOptions,
22
23
  CommonDBStreamOptions,
23
24
  DBDeleteByIdsOperation,
@@ -62,6 +63,7 @@ export type {
62
63
  CommonDaoHooks,
63
64
  CommonDBOptions,
64
65
  CommonDBSaveOptions,
66
+ CommonDBSaveMethod,
65
67
  CommonDBStreamOptions,
66
68
  CommonDBCreateOptions,
67
69
  CommonDB,
@@ -11,8 +11,9 @@ import {
11
11
  testItemTMSchema,
12
12
  TEST_TABLE,
13
13
  createTestItemBM,
14
+ testItemDBMJsonSchema,
14
15
  } from './test.model'
15
- import { getTestItemSchema, TestItemBM } from '.'
16
+ import { TestItemBM } from '.'
16
17
 
17
18
  export function runCommonDaoTest(
18
19
  db: CommonDB,
@@ -65,7 +66,7 @@ export function runCommonDaoTest(
65
66
  // CREATE TABLE, DROP
66
67
  if (createTable) {
67
68
  test('createTable, dropIfExists=true', async () => {
68
- await dao.createTable(getTestItemSchema(), { dropIfExists: true })
69
+ await dao.createTable(testItemDBMJsonSchema.build(), { dropIfExists: true })
69
70
  })
70
71
  }
71
72
 
@@ -5,9 +5,9 @@ import { DBQuery } from '../query/dbQuery'
5
5
  import {
6
6
  createTestItemDBM,
7
7
  createTestItemsDBM,
8
- getTestItemSchema,
9
8
  TestItemDBM,
10
9
  TEST_TABLE,
10
+ testItemDBMJsonSchema,
11
11
  } from './test.model'
12
12
  import { deepFreeze } from './test.util'
13
13
 
@@ -21,6 +21,8 @@ export interface CommonDBImplementationFeatures {
21
21
  dbQueryFilterIn?: boolean
22
22
  dbQueryOrder?: boolean
23
23
  dbQuerySelectFields?: boolean
24
+ insert?: boolean
25
+ update?: boolean
24
26
 
25
27
  createTable?: boolean
26
28
  tableSchemas?: boolean
@@ -80,6 +82,8 @@ export function runCommonDBTest(
80
82
  // dbQueryFilterIn = true,
81
83
  dbQueryOrder = true,
82
84
  dbQuerySelectFields = true,
85
+ insert = true,
86
+ update = true,
83
87
  streaming = true,
84
88
  strongConsistency = true,
85
89
  bufferSupport = true,
@@ -106,7 +110,7 @@ export function runCommonDBTest(
106
110
  // CREATE TABLE, DROP
107
111
  if (createTable) {
108
112
  test('createTable, dropIfExists=true', async () => {
109
- await db.createTable(TEST_TABLE, getTestItemSchema(), { dropIfExists: true })
113
+ await db.createTable(TEST_TABLE, testItemDBMJsonSchema.build(), { dropIfExists: true })
110
114
  })
111
115
  }
112
116
 
@@ -176,10 +180,28 @@ export function runCommonDBTest(
176
180
  })
177
181
  }
178
182
 
183
+ if (update) {
184
+ test('saveBatch UPDATE method should throw', async () => {
185
+ await expect(db.saveBatch(TEST_TABLE, items, { saveMethod: 'update' })).rejects.toThrow()
186
+ })
187
+ }
188
+
179
189
  test('saveBatch test items', async () => {
180
190
  await db.saveBatch(TEST_TABLE, items)
181
191
  })
182
192
 
193
+ if (insert) {
194
+ test('saveBatch INSERT method should throw', async () => {
195
+ await expect(db.saveBatch(TEST_TABLE, items, { saveMethod: 'insert' })).rejects.toThrow()
196
+ })
197
+ }
198
+
199
+ if (update) {
200
+ test('saveBatch UPDATE method should pass', async () => {
201
+ await db.saveBatch(TEST_TABLE, items, { saveMethod: 'update' })
202
+ })
203
+ }
204
+
183
205
  // GET not empty
184
206
  test('getByIds all items', async () => {
185
207
  const rows = await db.getByIds<TestItemDBM>(TEST_TABLE, items.map(i => i.id).concat('abcd'))
@@ -10,10 +10,11 @@ import {
10
10
  createTestItemDBM,
11
11
  createTestItemsBM,
12
12
  createTestItemsDBM,
13
- getTestItemSchema,
14
13
  TestItemBM,
14
+ testItemBMJsonSchema,
15
15
  testItemBMSchema,
16
16
  TestItemDBM,
17
+ testItemDBMJsonSchema,
17
18
  testItemDBMSchema,
18
19
  TestItemTM,
19
20
  testItemTMSchema,
@@ -37,7 +38,8 @@ export {
37
38
  testItemDBMSchema,
38
39
  testItemBMSchema,
39
40
  testItemTMSchema,
40
- getTestItemSchema,
41
+ testItemBMJsonSchema,
42
+ testItemDBMJsonSchema,
41
43
  runCommonDBTest,
42
44
  runCommonDaoTest,
43
45
  runCommonKeyValueDBTest,
@@ -1,4 +1,4 @@
1
- import { jsonSchema, JsonSchemaObject, _range, BaseDBEntity, Saved } from '@naturalcycles/js-lib'
1
+ import { jsonSchema, _range, BaseDBEntity, Saved } from '@naturalcycles/js-lib'
2
2
  import {
3
3
  baseDBEntitySchema,
4
4
  binarySchema,
@@ -94,20 +94,3 @@ export function createTestItemsDBM(count = 1): TestItemDBM[] {
94
94
  export function createTestItemsBM(count = 1): Saved<TestItemBM>[] {
95
95
  return _range(1, count + 1).map(num => createTestItemBM(num))
96
96
  }
97
-
98
- const testItemJsonSchema = jsonSchema
99
- .object<TestItemDBM>({
100
- id: jsonSchema.string(),
101
- k1: jsonSchema.string(),
102
- k2: jsonSchema.string(),
103
- k3: jsonSchema.number(),
104
- even: jsonSchema.boolean(),
105
- created: jsonSchema.unixTimestamp(),
106
- updated: jsonSchema.unixTimestamp(),
107
- })
108
- .build()
109
-
110
- export function getTestItemSchema(): JsonSchemaObject<TestItemDBM> {
111
- // return CommonSchemaGenerator.generateFromRows({ table: TEST_TABLE }, createTestItemsDBM())
112
- return testItemJsonSchema
113
- }