@naturalcycles/db-lib 10.45.2 → 10.46.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -123,16 +123,16 @@ export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity
|
|
|
123
123
|
* @experimental
|
|
124
124
|
*/
|
|
125
125
|
incrementBatch(prop: keyof DBM, incrementMap: StringMap<number>, opt?: CommonDaoOptions): Promise<StringMap<number>>;
|
|
126
|
-
dbmToBM(_dbm: undefined, opt?: CommonDaoOptions):
|
|
127
|
-
dbmToBM(_dbm?: DBM, opt?: CommonDaoOptions):
|
|
128
|
-
dbmsToBM(dbms: DBM[], opt?: CommonDaoOptions):
|
|
126
|
+
dbmToBM(_dbm: undefined, opt?: CommonDaoOptions): null;
|
|
127
|
+
dbmToBM(_dbm?: DBM, opt?: CommonDaoOptions): BM;
|
|
128
|
+
dbmsToBM(dbms: DBM[], opt?: CommonDaoOptions): BM[];
|
|
129
129
|
/**
|
|
130
130
|
* Mutates object with properties: id, created, updated.
|
|
131
131
|
* Returns DBM (new reference).
|
|
132
132
|
*/
|
|
133
|
-
bmToDBM(bm: undefined, opt?: CommonDaoOptions):
|
|
134
|
-
bmToDBM(bm?: BM, opt?: CommonDaoOptions):
|
|
135
|
-
bmsToDBM(bms: BM[], opt?: CommonDaoOptions):
|
|
133
|
+
bmToDBM(bm: undefined, opt?: CommonDaoOptions): null;
|
|
134
|
+
bmToDBM(bm?: BM, opt?: CommonDaoOptions): DBM;
|
|
135
|
+
bmsToDBM(bms: BM[], opt?: CommonDaoOptions): DBM[];
|
|
136
136
|
/**
|
|
137
137
|
* Converts a DBM to storage format, applying compression if configured.
|
|
138
138
|
*
|
|
@@ -164,9 +164,9 @@ export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity
|
|
|
164
164
|
storageRowsToDBMs(rows: ObjectWithId[]): Promise<DBM[]>;
|
|
165
165
|
private compress;
|
|
166
166
|
private decompress;
|
|
167
|
-
anyToDBM(dbm: undefined, opt?: CommonDaoOptions):
|
|
168
|
-
anyToDBM(dbm?: any, opt?: CommonDaoOptions):
|
|
169
|
-
anyToDBMs(rows: DBM[], opt?: CommonDaoOptions):
|
|
167
|
+
anyToDBM(dbm: undefined, opt?: CommonDaoOptions): null;
|
|
168
|
+
anyToDBM(dbm?: any, opt?: CommonDaoOptions): DBM;
|
|
169
|
+
anyToDBMs(rows: DBM[], opt?: CommonDaoOptions): DBM[];
|
|
170
170
|
/**
|
|
171
171
|
* Returns *converted value* (NOT the same reference).
|
|
172
172
|
* Does NOT mutate the object.
|
|
@@ -183,43 +183,6 @@ export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity
|
|
|
183
183
|
withIds(ids: ID[]): DaoWithIds<CommonDao<BM, DBM, ID>>;
|
|
184
184
|
withRowsToSave(rows: Unsaved<BM>[]): DaoWithRows<CommonDao<BM, DBM, ID>>;
|
|
185
185
|
withRowToSave(row: Unsaved<BM>, opt?: DaoWithRowOptions<BM>): DaoWithRow<CommonDao<BM, DBM, ID>>;
|
|
186
|
-
/**
|
|
187
|
-
* Helper to decompress legacy compressed data when migrating away from auto-compression.
|
|
188
|
-
* Use as your `beforeDBMToBM` hook to decompress legacy rows on read.
|
|
189
|
-
*
|
|
190
|
-
* @example
|
|
191
|
-
* const dao = new CommonDao({
|
|
192
|
-
* hooks: {
|
|
193
|
-
* beforeDBMToBM: CommonDao.decompressLegacyRow,
|
|
194
|
-
* }
|
|
195
|
-
* })
|
|
196
|
-
*
|
|
197
|
-
* // Or within an existing hook:
|
|
198
|
-
* beforeDBMToBM: async (dbm) => {
|
|
199
|
-
* await CommonDao.decompressLegacyRow(dbm)
|
|
200
|
-
* // ... other transformations
|
|
201
|
-
* return dbm
|
|
202
|
-
* }
|
|
203
|
-
*/
|
|
204
|
-
static decompressLegacyRow<T extends ObjectWithId>(this: void, row: T): Promise<T>;
|
|
205
|
-
/**
|
|
206
|
-
* Temporary helper to migrate from the old `data` compressed property to the new `__compressed` property.
|
|
207
|
-
* Use as your `beforeDBMToBM` hook during the migration period.
|
|
208
|
-
*
|
|
209
|
-
* Migration steps:
|
|
210
|
-
* 1. Add `beforeDBMToBM: CommonDao.migrateCompressedDataProperty` to your hooks
|
|
211
|
-
* 2. Deploy - old data (with `data` property) will be decompressed on read and recompressed to `__compressed` on write
|
|
212
|
-
* 3. Once all data has been naturally rewritten, remove the hook
|
|
213
|
-
*
|
|
214
|
-
* @example
|
|
215
|
-
* const dao = new CommonDao({
|
|
216
|
-
* compress: { keys: ['field1', 'field2'] },
|
|
217
|
-
* hooks: {
|
|
218
|
-
* beforeDBMToBM: CommonDao.migrateCompressedDataProperty,
|
|
219
|
-
* }
|
|
220
|
-
* })
|
|
221
|
-
*/
|
|
222
|
-
static migrateCompressedDataProperty<T extends ObjectWithId>(row: T): Promise<T>;
|
|
223
186
|
/**
|
|
224
187
|
* Load rows (by their ids) from Multiple tables at once.
|
|
225
188
|
* An optimized way to load data, minimizing DB round-trips.
|
|
@@ -83,21 +83,21 @@ export class CommonDao {
|
|
|
83
83
|
if (!id)
|
|
84
84
|
return null;
|
|
85
85
|
const [dbm] = await this.loadByIds([id], opt);
|
|
86
|
-
return
|
|
86
|
+
return this.dbmToBM(dbm, opt);
|
|
87
87
|
}
|
|
88
88
|
async getByIdAsDBM(id, opt = {}) {
|
|
89
89
|
if (!id)
|
|
90
90
|
return null;
|
|
91
91
|
const [row] = await this.loadByIds([id], opt);
|
|
92
|
-
return
|
|
92
|
+
return this.anyToDBM(row, opt);
|
|
93
93
|
}
|
|
94
94
|
async getByIds(ids, opt = {}) {
|
|
95
95
|
const dbms = await this.loadByIds(ids, opt);
|
|
96
|
-
return
|
|
96
|
+
return this.dbmsToBM(dbms, opt);
|
|
97
97
|
}
|
|
98
98
|
async getByIdsAsDBM(ids, opt = {}) {
|
|
99
99
|
const rows = await this.loadByIds(ids, opt);
|
|
100
|
-
return
|
|
100
|
+
return this.anyToDBMs(rows);
|
|
101
101
|
}
|
|
102
102
|
// DRY private method
|
|
103
103
|
async loadByIds(ids, opt = {}) {
|
|
@@ -149,7 +149,7 @@ export class CommonDao {
|
|
|
149
149
|
const { rows: rawRows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
|
|
150
150
|
const isPartialQuery = !!q._selectedFieldNames;
|
|
151
151
|
const rows = isPartialQuery ? rawRows : await this.storageRowsToDBMs(rawRows);
|
|
152
|
-
const bms = isPartialQuery ? rows :
|
|
152
|
+
const bms = isPartialQuery ? rows : this.dbmsToBM(rows, opt);
|
|
153
153
|
return {
|
|
154
154
|
rows: bms,
|
|
155
155
|
...queryResult,
|
|
@@ -165,7 +165,7 @@ export class CommonDao {
|
|
|
165
165
|
const { rows: rawRows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
|
|
166
166
|
const isPartialQuery = !!q._selectedFieldNames;
|
|
167
167
|
const rows = isPartialQuery ? rawRows : await this.storageRowsToDBMs(rawRows);
|
|
168
|
-
const dbms = isPartialQuery ? rows :
|
|
168
|
+
const dbms = isPartialQuery ? rows : this.anyToDBMs(rows, opt);
|
|
169
169
|
return { rows: dbms, ...queryResult };
|
|
170
170
|
}
|
|
171
171
|
async runQueryCount(q, opt = {}) {
|
|
@@ -185,7 +185,7 @@ export class CommonDao {
|
|
|
185
185
|
return pipeline;
|
|
186
186
|
opt.skipValidation ??= true;
|
|
187
187
|
opt.errorMode ||= ErrorMode.SUPPRESS;
|
|
188
|
-
return pipeline.
|
|
188
|
+
return pipeline.mapSync(dbm => this.anyToDBM(dbm, opt), { errorMode: opt.errorMode });
|
|
189
189
|
}
|
|
190
190
|
streamQuery(q, opt = {}) {
|
|
191
191
|
this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
|
|
@@ -199,7 +199,7 @@ export class CommonDao {
|
|
|
199
199
|
return pipeline;
|
|
200
200
|
opt.skipValidation ??= true;
|
|
201
201
|
opt.errorMode ||= ErrorMode.SUPPRESS;
|
|
202
|
-
return pipeline.
|
|
202
|
+
return pipeline.mapSync(dbm => this.dbmToBM(dbm, opt), { errorMode: opt.errorMode });
|
|
203
203
|
}
|
|
204
204
|
async queryIds(q, opt = {}) {
|
|
205
205
|
this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
|
|
@@ -347,7 +347,7 @@ export class CommonDao {
|
|
|
347
347
|
}
|
|
348
348
|
this.assignIdCreatedUpdated(bm, opt); // mutates
|
|
349
349
|
_typeCast(bm);
|
|
350
|
-
const dbm =
|
|
350
|
+
const dbm = this.bmToDBM(bm, opt); // validates BM
|
|
351
351
|
this.cfg.hooks.beforeSave?.(dbm);
|
|
352
352
|
const table = opt.table || this.cfg.table;
|
|
353
353
|
const saveOptions = this.prepareSaveOptions(opt);
|
|
@@ -361,7 +361,7 @@ export class CommonDao {
|
|
|
361
361
|
async saveAsDBM(dbm, opt = {}) {
|
|
362
362
|
this.requireWriteAccess();
|
|
363
363
|
this.assignIdCreatedUpdated(dbm, opt); // mutates
|
|
364
|
-
const validDbm =
|
|
364
|
+
const validDbm = this.anyToDBM(dbm, opt);
|
|
365
365
|
this.cfg.hooks.beforeSave?.(validDbm);
|
|
366
366
|
const table = opt.table || this.cfg.table;
|
|
367
367
|
const saveOptions = this.prepareSaveOptions(opt);
|
|
@@ -377,7 +377,7 @@ export class CommonDao {
|
|
|
377
377
|
return [];
|
|
378
378
|
this.requireWriteAccess();
|
|
379
379
|
bms.forEach(bm => this.assignIdCreatedUpdated(bm, opt));
|
|
380
|
-
const dbms =
|
|
380
|
+
const dbms = this.bmsToDBM(bms, opt);
|
|
381
381
|
if (this.cfg.hooks.beforeSave) {
|
|
382
382
|
dbms.forEach(dbm => this.cfg.hooks.beforeSave(dbm));
|
|
383
383
|
}
|
|
@@ -395,7 +395,7 @@ export class CommonDao {
|
|
|
395
395
|
return [];
|
|
396
396
|
this.requireWriteAccess();
|
|
397
397
|
dbms.forEach(dbm => this.assignIdCreatedUpdated(dbm, opt));
|
|
398
|
-
const validDbms =
|
|
398
|
+
const validDbms = this.anyToDBMs(dbms, opt);
|
|
399
399
|
if (this.cfg.hooks.beforeSave) {
|
|
400
400
|
validDbms.forEach(dbm => this.cfg.hooks.beforeSave(dbm));
|
|
401
401
|
}
|
|
@@ -449,7 +449,7 @@ export class CommonDao {
|
|
|
449
449
|
await p
|
|
450
450
|
.map(async bm => {
|
|
451
451
|
this.assignIdCreatedUpdated(bm, opt);
|
|
452
|
-
const dbm =
|
|
452
|
+
const dbm = this.bmToDBM(bm, opt);
|
|
453
453
|
beforeSave?.(dbm);
|
|
454
454
|
return await this.dbmToStorageRow(dbm);
|
|
455
455
|
}, { errorMode })
|
|
@@ -560,32 +560,31 @@ export class CommonDao {
|
|
|
560
560
|
const { table } = this.cfg;
|
|
561
561
|
return await this.cfg.db.incrementBatch(table, prop, incrementMap);
|
|
562
562
|
}
|
|
563
|
-
|
|
563
|
+
dbmToBM(_dbm, opt = {}) {
|
|
564
564
|
if (!_dbm)
|
|
565
565
|
return null;
|
|
566
566
|
// optimization: no need to run full joi DBM validation, cause BM validation will be run
|
|
567
567
|
// const dbm = this.anyToDBM(_dbm, opt)
|
|
568
568
|
const dbm = { ..._dbm, ...this.cfg.hooks.parseNaturalId(_dbm.id) };
|
|
569
569
|
// DBM > BM
|
|
570
|
-
const bm = (
|
|
570
|
+
const bm = (this.cfg.hooks.beforeDBMToBM?.(dbm) || dbm);
|
|
571
571
|
// Validate/convert BM
|
|
572
572
|
return this.validateAndConvert(bm, 'load', opt);
|
|
573
573
|
}
|
|
574
|
-
|
|
575
|
-
return
|
|
574
|
+
dbmsToBM(dbms, opt = {}) {
|
|
575
|
+
return dbms.map(dbm => this.dbmToBM(dbm, opt));
|
|
576
576
|
}
|
|
577
|
-
|
|
577
|
+
bmToDBM(bm, opt) {
|
|
578
578
|
if (bm === undefined)
|
|
579
579
|
return null;
|
|
580
580
|
// bm gets assigned to the new reference
|
|
581
581
|
bm = this.validateAndConvert(bm, 'save', opt);
|
|
582
582
|
// BM > DBM
|
|
583
|
-
const dbm = (
|
|
583
|
+
const dbm = (this.cfg.hooks.beforeBMToDBM?.(bm) || bm);
|
|
584
584
|
return dbm;
|
|
585
585
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
return await pMap(bms, async bm => await this.bmToDBM(bm, opt));
|
|
586
|
+
bmsToDBM(bms, opt = {}) {
|
|
587
|
+
return bms.map(bm => this.bmToDBM(bm, opt));
|
|
589
588
|
}
|
|
590
589
|
// STORAGE LAYER (compression/decompression at DB boundary)
|
|
591
590
|
// These methods convert between DBM (logical model) and storage format (physical, possibly compressed).
|
|
@@ -660,15 +659,13 @@ export class CommonDao {
|
|
|
660
659
|
_typeCast(dbm);
|
|
661
660
|
if (!Buffer.isBuffer(dbm.__compressed))
|
|
662
661
|
return; // No compressed data
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
catch { }
|
|
662
|
+
// todo: stop supporting Inflate when we are sure that we have migrated everything to zstd
|
|
663
|
+
const bufferString = await decompressZstdOrInflateToString(dbm.__compressed);
|
|
664
|
+
const properties = JSON.parse(bufferString);
|
|
665
|
+
dbm.__compressed = undefined;
|
|
666
|
+
Object.assign(dbm, properties);
|
|
670
667
|
}
|
|
671
|
-
|
|
668
|
+
anyToDBM(dbm, _opt = {}) {
|
|
672
669
|
if (!dbm)
|
|
673
670
|
return null;
|
|
674
671
|
// this shouldn't be happening on load! but should on save!
|
|
@@ -678,8 +675,8 @@ export class CommonDao {
|
|
|
678
675
|
// return this.validateAndConvert(dbm, this.cfg.dbmSchema, DBModelType.DBM, opt)
|
|
679
676
|
return dbm;
|
|
680
677
|
}
|
|
681
|
-
|
|
682
|
-
return
|
|
678
|
+
anyToDBMs(rows, opt = {}) {
|
|
679
|
+
return rows.map(entity => this.anyToDBM(entity, opt));
|
|
683
680
|
}
|
|
684
681
|
/**
|
|
685
682
|
* Returns *converted value* (NOT the same reference).
|
|
@@ -751,73 +748,6 @@ export class CommonDao {
|
|
|
751
748
|
opt: opt,
|
|
752
749
|
};
|
|
753
750
|
}
|
|
754
|
-
/**
|
|
755
|
-
* Helper to decompress legacy compressed data when migrating away from auto-compression.
|
|
756
|
-
* Use as your `beforeDBMToBM` hook to decompress legacy rows on read.
|
|
757
|
-
*
|
|
758
|
-
* @example
|
|
759
|
-
* const dao = new CommonDao({
|
|
760
|
-
* hooks: {
|
|
761
|
-
* beforeDBMToBM: CommonDao.decompressLegacyRow,
|
|
762
|
-
* }
|
|
763
|
-
* })
|
|
764
|
-
*
|
|
765
|
-
* // Or within an existing hook:
|
|
766
|
-
* beforeDBMToBM: async (dbm) => {
|
|
767
|
-
* await CommonDao.decompressLegacyRow(dbm)
|
|
768
|
-
* // ... other transformations
|
|
769
|
-
* return dbm
|
|
770
|
-
* }
|
|
771
|
-
*/
|
|
772
|
-
static async decompressLegacyRow(row) {
|
|
773
|
-
// Check both __compressed (current) and data (legacy) for backward compatibility
|
|
774
|
-
const compressed = row.__compressed ?? row.data;
|
|
775
|
-
if (!Buffer.isBuffer(compressed))
|
|
776
|
-
return row;
|
|
777
|
-
try {
|
|
778
|
-
const bufferString = await decompressZstdOrInflateToString(compressed);
|
|
779
|
-
const properties = JSON.parse(bufferString);
|
|
780
|
-
row.__compressed = undefined;
|
|
781
|
-
row.data = undefined;
|
|
782
|
-
Object.assign(row, properties);
|
|
783
|
-
}
|
|
784
|
-
catch {
|
|
785
|
-
// Decompression failed - field is not compressed, leave as-is
|
|
786
|
-
}
|
|
787
|
-
return row;
|
|
788
|
-
}
|
|
789
|
-
/**
|
|
790
|
-
* Temporary helper to migrate from the old `data` compressed property to the new `__compressed` property.
|
|
791
|
-
* Use as your `beforeDBMToBM` hook during the migration period.
|
|
792
|
-
*
|
|
793
|
-
* Migration steps:
|
|
794
|
-
* 1. Add `beforeDBMToBM: CommonDao.migrateCompressedDataProperty` to your hooks
|
|
795
|
-
* 2. Deploy - old data (with `data` property) will be decompressed on read and recompressed to `__compressed` on write
|
|
796
|
-
* 3. Once all data has been naturally rewritten, remove the hook
|
|
797
|
-
*
|
|
798
|
-
* @example
|
|
799
|
-
* const dao = new CommonDao({
|
|
800
|
-
* compress: { keys: ['field1', 'field2'] },
|
|
801
|
-
* hooks: {
|
|
802
|
-
* beforeDBMToBM: CommonDao.migrateCompressedDataProperty,
|
|
803
|
-
* }
|
|
804
|
-
* })
|
|
805
|
-
*/
|
|
806
|
-
static async migrateCompressedDataProperty(row) {
|
|
807
|
-
const data = row.data;
|
|
808
|
-
if (!Buffer.isBuffer(data))
|
|
809
|
-
return row;
|
|
810
|
-
try {
|
|
811
|
-
const bufferString = await decompressZstdOrInflateToString(data);
|
|
812
|
-
const properties = JSON.parse(bufferString);
|
|
813
|
-
row.data = undefined;
|
|
814
|
-
Object.assign(row, properties);
|
|
815
|
-
}
|
|
816
|
-
catch {
|
|
817
|
-
// Decompression failed - data field is not compressed, leave as-is
|
|
818
|
-
}
|
|
819
|
-
return row;
|
|
820
|
-
}
|
|
821
751
|
/**
|
|
822
752
|
* Load rows (by their ids) from Multiple tables at once.
|
|
823
753
|
* An optimized way to load data, minimizing DB round-trips.
|
|
@@ -881,7 +811,7 @@ export class CommonDao {
|
|
|
881
811
|
const row = dbmByTableById[table][input.id];
|
|
882
812
|
// Decompress before converting to BM
|
|
883
813
|
const dbm = row ? await dao.storageRowToDBM(row) : undefined;
|
|
884
|
-
bmsByProp[prop] =
|
|
814
|
+
bmsByProp[prop] = dao.dbmToBM(dbm, opt) || null;
|
|
885
815
|
}
|
|
886
816
|
else {
|
|
887
817
|
// Plural
|
|
@@ -890,7 +820,7 @@ export class CommonDao {
|
|
|
890
820
|
const rows = input.ids.map(id => dbmByTableById[table][id]).filter(_isTruthy);
|
|
891
821
|
// Decompress before converting to BM
|
|
892
822
|
const dbms = await dao.storageRowsToDBMs(rows);
|
|
893
|
-
bmsByProp[prop] =
|
|
823
|
+
bmsByProp[prop] = dao.dbmsToBM(dbms, opt);
|
|
894
824
|
}
|
|
895
825
|
});
|
|
896
826
|
return bmsByProp;
|
|
@@ -941,7 +871,7 @@ export class CommonDao {
|
|
|
941
871
|
}
|
|
942
872
|
}
|
|
943
873
|
dao.assignIdCreatedUpdated(row, opt);
|
|
944
|
-
const dbm =
|
|
874
|
+
const dbm = dao.bmToDBM(row, opt);
|
|
945
875
|
dao.cfg.hooks.beforeSave?.(dbm);
|
|
946
876
|
const storageRow = await dao.dbmToStorageRow(dbm);
|
|
947
877
|
dbmsByTable[table].push(storageRow);
|
|
@@ -949,7 +879,7 @@ export class CommonDao {
|
|
|
949
879
|
else {
|
|
950
880
|
// Plural
|
|
951
881
|
input.rows.forEach(bm => dao.assignIdCreatedUpdated(bm, opt));
|
|
952
|
-
const dbms =
|
|
882
|
+
const dbms = dao.bmsToDBM(input.rows, opt);
|
|
953
883
|
if (dao.cfg.hooks.beforeSave) {
|
|
954
884
|
dbms.forEach(dbm => dao.cfg.hooks.beforeSave(dbm));
|
|
955
885
|
}
|
|
@@ -31,8 +31,8 @@ export interface CommonDaoHooks<BM extends BaseDBEntity, DBM extends BaseDBEntit
|
|
|
31
31
|
* - patch, patchAsDBM
|
|
32
32
|
*/
|
|
33
33
|
beforeCreate: (bm: Partial<BM>) => Partial<BM>;
|
|
34
|
-
beforeDBMToBM: (dbm: DBM) => Partial<BM
|
|
35
|
-
beforeBMToDBM: (bm: BM) => Partial<DBM
|
|
34
|
+
beforeDBMToBM: (dbm: DBM) => Partial<BM>;
|
|
35
|
+
beforeBMToDBM: (bm: BM) => Partial<DBM>;
|
|
36
36
|
/**
|
|
37
37
|
* Allows to access the DBM just after it has been loaded from the DB.
|
|
38
38
|
*
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/db-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "10.
|
|
4
|
+
"version": "10.46.1",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/js-lib": "^15",
|
|
7
7
|
"@naturalcycles/nodejs-lib": "^15"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
10
|
+
"@typescript/native-preview": "7.0.0-dev.20260301.1",
|
|
11
11
|
"@naturalcycles/dev-lib": "18.4.2"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
@@ -40,8 +40,8 @@ export interface CommonDaoHooks<
|
|
|
40
40
|
*/
|
|
41
41
|
beforeCreate: (bm: Partial<BM>) => Partial<BM>
|
|
42
42
|
|
|
43
|
-
beforeDBMToBM: (dbm: DBM) => Partial<BM>
|
|
44
|
-
beforeBMToDBM: (bm: BM) => Partial<DBM>
|
|
43
|
+
beforeDBMToBM: (dbm: DBM) => Partial<BM>
|
|
44
|
+
beforeBMToDBM: (bm: BM) => Partial<DBM>
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Allows to access the DBM just after it has been loaded from the DB.
|
|
@@ -131,23 +131,23 @@ export class CommonDao<
|
|
|
131
131
|
async getById(id?: ID | null, opt: CommonDaoReadOptions = {}): Promise<BM | null> {
|
|
132
132
|
if (!id) return null
|
|
133
133
|
const [dbm] = await this.loadByIds([id], opt)
|
|
134
|
-
return
|
|
134
|
+
return this.dbmToBM(dbm, opt)
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
async getByIdAsDBM(id?: ID | null, opt: CommonDaoReadOptions = {}): Promise<DBM | null> {
|
|
138
138
|
if (!id) return null
|
|
139
139
|
const [row] = await this.loadByIds([id], opt)
|
|
140
|
-
return
|
|
140
|
+
return this.anyToDBM(row, opt)
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
async getByIds(ids: ID[], opt: CommonDaoReadOptions = {}): Promise<BM[]> {
|
|
144
144
|
const dbms = await this.loadByIds(ids, opt)
|
|
145
|
-
return
|
|
145
|
+
return this.dbmsToBM(dbms, opt)
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
async getByIdsAsDBM(ids: ID[], opt: CommonDaoReadOptions = {}): Promise<DBM[]> {
|
|
149
149
|
const rows = await this.loadByIds(ids, opt)
|
|
150
|
-
return
|
|
150
|
+
return this.anyToDBMs(rows)
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
// DRY private method
|
|
@@ -217,7 +217,7 @@ export class CommonDao<
|
|
|
217
217
|
const { rows: rawRows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
|
|
218
218
|
const isPartialQuery = !!q._selectedFieldNames
|
|
219
219
|
const rows = isPartialQuery ? rawRows : await this.storageRowsToDBMs(rawRows)
|
|
220
|
-
const bms = isPartialQuery ? (rows as any[]) :
|
|
220
|
+
const bms = isPartialQuery ? (rows as any[]) : this.dbmsToBM(rows, opt)
|
|
221
221
|
return {
|
|
222
222
|
rows: bms,
|
|
223
223
|
...queryResult,
|
|
@@ -238,7 +238,7 @@ export class CommonDao<
|
|
|
238
238
|
const { rows: rawRows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
|
|
239
239
|
const isPartialQuery = !!q._selectedFieldNames
|
|
240
240
|
const rows = isPartialQuery ? rawRows : await this.storageRowsToDBMs(rawRows)
|
|
241
|
-
const dbms = isPartialQuery ? rows :
|
|
241
|
+
const dbms = isPartialQuery ? rows : this.anyToDBMs(rows, opt)
|
|
242
242
|
return { rows: dbms, ...queryResult }
|
|
243
243
|
}
|
|
244
244
|
|
|
@@ -263,7 +263,7 @@ export class CommonDao<
|
|
|
263
263
|
opt.skipValidation ??= true
|
|
264
264
|
opt.errorMode ||= ErrorMode.SUPPRESS
|
|
265
265
|
|
|
266
|
-
return pipeline.
|
|
266
|
+
return pipeline.mapSync(dbm => this.anyToDBM(dbm, opt), { errorMode: opt.errorMode })
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
streamQuery(q: DBQuery<DBM>, opt: CommonDaoStreamOptions<BM> = {}): Pipeline<BM> {
|
|
@@ -281,7 +281,7 @@ export class CommonDao<
|
|
|
281
281
|
opt.skipValidation ??= true
|
|
282
282
|
opt.errorMode ||= ErrorMode.SUPPRESS
|
|
283
283
|
|
|
284
|
-
return pipeline.
|
|
284
|
+
return pipeline.mapSync(dbm => this.dbmToBM(dbm, opt), { errorMode: opt.errorMode })
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
async queryIds(q: DBQuery<DBM>, opt: CommonDaoReadOptions = {}): Promise<ID[]> {
|
|
@@ -466,7 +466,7 @@ export class CommonDao<
|
|
|
466
466
|
|
|
467
467
|
this.assignIdCreatedUpdated(bm, opt) // mutates
|
|
468
468
|
_typeCast<BM>(bm)
|
|
469
|
-
const dbm =
|
|
469
|
+
const dbm = this.bmToDBM(bm, opt) // validates BM
|
|
470
470
|
this.cfg.hooks!.beforeSave?.(dbm)
|
|
471
471
|
const table = opt.table || this.cfg.table
|
|
472
472
|
const saveOptions = this.prepareSaveOptions(opt)
|
|
@@ -484,7 +484,7 @@ export class CommonDao<
|
|
|
484
484
|
async saveAsDBM(dbm: Unsaved<DBM>, opt: CommonDaoSaveOptions<BM, DBM> = {}): Promise<DBM> {
|
|
485
485
|
this.requireWriteAccess()
|
|
486
486
|
this.assignIdCreatedUpdated(dbm, opt) // mutates
|
|
487
|
-
const validDbm =
|
|
487
|
+
const validDbm = this.anyToDBM(dbm, opt)
|
|
488
488
|
this.cfg.hooks!.beforeSave?.(validDbm)
|
|
489
489
|
const table = opt.table || this.cfg.table
|
|
490
490
|
const saveOptions = this.prepareSaveOptions(opt)
|
|
@@ -503,7 +503,7 @@ export class CommonDao<
|
|
|
503
503
|
if (!bms.length) return []
|
|
504
504
|
this.requireWriteAccess()
|
|
505
505
|
bms.forEach(bm => this.assignIdCreatedUpdated(bm, opt))
|
|
506
|
-
const dbms =
|
|
506
|
+
const dbms = this.bmsToDBM(bms as BM[], opt)
|
|
507
507
|
if (this.cfg.hooks!.beforeSave) {
|
|
508
508
|
dbms.forEach(dbm => this.cfg.hooks!.beforeSave!(dbm))
|
|
509
509
|
}
|
|
@@ -527,7 +527,7 @@ export class CommonDao<
|
|
|
527
527
|
if (!dbms.length) return []
|
|
528
528
|
this.requireWriteAccess()
|
|
529
529
|
dbms.forEach(dbm => this.assignIdCreatedUpdated(dbm, opt))
|
|
530
|
-
const validDbms =
|
|
530
|
+
const validDbms = this.anyToDBMs(dbms as DBM[], opt)
|
|
531
531
|
if (this.cfg.hooks!.beforeSave) {
|
|
532
532
|
validDbms.forEach(dbm => this.cfg.hooks!.beforeSave!(dbm))
|
|
533
533
|
}
|
|
@@ -600,7 +600,7 @@ export class CommonDao<
|
|
|
600
600
|
.map(
|
|
601
601
|
async bm => {
|
|
602
602
|
this.assignIdCreatedUpdated(bm, opt)
|
|
603
|
-
const dbm =
|
|
603
|
+
const dbm = this.bmToDBM(bm, opt)
|
|
604
604
|
beforeSave?.(dbm)
|
|
605
605
|
return await this.dbmToStorageRow(dbm)
|
|
606
606
|
},
|
|
@@ -739,9 +739,9 @@ export class CommonDao<
|
|
|
739
739
|
|
|
740
740
|
// CONVERSIONS
|
|
741
741
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
742
|
+
dbmToBM(_dbm: undefined, opt?: CommonDaoOptions): null
|
|
743
|
+
dbmToBM(_dbm?: DBM, opt?: CommonDaoOptions): BM
|
|
744
|
+
dbmToBM(_dbm?: DBM, opt: CommonDaoOptions = {}): BM | null {
|
|
745
745
|
if (!_dbm) return null
|
|
746
746
|
|
|
747
747
|
// optimization: no need to run full joi DBM validation, cause BM validation will be run
|
|
@@ -749,37 +749,36 @@ export class CommonDao<
|
|
|
749
749
|
const dbm: DBM = { ..._dbm, ...this.cfg.hooks!.parseNaturalId!(_dbm.id as ID) }
|
|
750
750
|
|
|
751
751
|
// DBM > BM
|
|
752
|
-
const bm = (
|
|
752
|
+
const bm = (this.cfg.hooks!.beforeDBMToBM?.(dbm) || dbm) as Partial<BM>
|
|
753
753
|
|
|
754
754
|
// Validate/convert BM
|
|
755
755
|
return this.validateAndConvert(bm, 'load', opt)
|
|
756
756
|
}
|
|
757
757
|
|
|
758
|
-
|
|
759
|
-
return
|
|
758
|
+
dbmsToBM(dbms: DBM[], opt: CommonDaoOptions = {}): BM[] {
|
|
759
|
+
return dbms.map(dbm => this.dbmToBM(dbm, opt))
|
|
760
760
|
}
|
|
761
761
|
|
|
762
762
|
/**
|
|
763
763
|
* Mutates object with properties: id, created, updated.
|
|
764
764
|
* Returns DBM (new reference).
|
|
765
765
|
*/
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
766
|
+
bmToDBM(bm: undefined, opt?: CommonDaoOptions): null
|
|
767
|
+
bmToDBM(bm?: BM, opt?: CommonDaoOptions): DBM
|
|
768
|
+
bmToDBM(bm?: BM, opt?: CommonDaoOptions): DBM | null {
|
|
769
769
|
if (bm === undefined) return null
|
|
770
770
|
|
|
771
771
|
// bm gets assigned to the new reference
|
|
772
772
|
bm = this.validateAndConvert(bm, 'save', opt)
|
|
773
773
|
|
|
774
774
|
// BM > DBM
|
|
775
|
-
const dbm = (
|
|
775
|
+
const dbm = (this.cfg.hooks!.beforeBMToDBM?.(bm) || bm) as DBM
|
|
776
776
|
|
|
777
777
|
return dbm
|
|
778
778
|
}
|
|
779
779
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
return await pMap(bms, async bm => await this.bmToDBM(bm, opt))
|
|
780
|
+
bmsToDBM(bms: BM[], opt: CommonDaoOptions = {}): DBM[] {
|
|
781
|
+
return bms.map(bm => this.bmToDBM(bm, opt))
|
|
783
782
|
}
|
|
784
783
|
|
|
785
784
|
// STORAGE LAYER (compression/decompression at DB boundary)
|
|
@@ -857,17 +856,16 @@ export class CommonDao<
|
|
|
857
856
|
_typeCast<Compressed<DBM>>(dbm)
|
|
858
857
|
if (!Buffer.isBuffer(dbm.__compressed)) return // No compressed data
|
|
859
858
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
} catch {}
|
|
859
|
+
// todo: stop supporting Inflate when we are sure that we have migrated everything to zstd
|
|
860
|
+
const bufferString = await decompressZstdOrInflateToString(dbm.__compressed)
|
|
861
|
+
const properties = JSON.parse(bufferString)
|
|
862
|
+
dbm.__compressed = undefined
|
|
863
|
+
Object.assign(dbm, properties)
|
|
866
864
|
}
|
|
867
865
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
866
|
+
anyToDBM(dbm: undefined, opt?: CommonDaoOptions): null
|
|
867
|
+
anyToDBM(dbm?: any, opt?: CommonDaoOptions): DBM
|
|
868
|
+
anyToDBM(dbm?: DBM, _opt: CommonDaoOptions = {}): DBM | null {
|
|
871
869
|
if (!dbm) return null
|
|
872
870
|
|
|
873
871
|
// this shouldn't be happening on load! but should on save!
|
|
@@ -880,8 +878,8 @@ export class CommonDao<
|
|
|
880
878
|
return dbm
|
|
881
879
|
}
|
|
882
880
|
|
|
883
|
-
|
|
884
|
-
return
|
|
881
|
+
anyToDBMs(rows: DBM[], opt: CommonDaoOptions = {}): DBM[] {
|
|
882
|
+
return rows.map(entity => this.anyToDBM(entity, opt))
|
|
885
883
|
}
|
|
886
884
|
|
|
887
885
|
/**
|
|
@@ -971,75 +969,6 @@ export class CommonDao<
|
|
|
971
969
|
}
|
|
972
970
|
}
|
|
973
971
|
|
|
974
|
-
/**
|
|
975
|
-
* Helper to decompress legacy compressed data when migrating away from auto-compression.
|
|
976
|
-
* Use as your `beforeDBMToBM` hook to decompress legacy rows on read.
|
|
977
|
-
*
|
|
978
|
-
* @example
|
|
979
|
-
* const dao = new CommonDao({
|
|
980
|
-
* hooks: {
|
|
981
|
-
* beforeDBMToBM: CommonDao.decompressLegacyRow,
|
|
982
|
-
* }
|
|
983
|
-
* })
|
|
984
|
-
*
|
|
985
|
-
* // Or within an existing hook:
|
|
986
|
-
* beforeDBMToBM: async (dbm) => {
|
|
987
|
-
* await CommonDao.decompressLegacyRow(dbm)
|
|
988
|
-
* // ... other transformations
|
|
989
|
-
* return dbm
|
|
990
|
-
* }
|
|
991
|
-
*/
|
|
992
|
-
static async decompressLegacyRow<T extends ObjectWithId>(this: void, row: T): Promise<T> {
|
|
993
|
-
// Check both __compressed (current) and data (legacy) for backward compatibility
|
|
994
|
-
const compressed = (row as any).__compressed ?? (row as any).data
|
|
995
|
-
if (!Buffer.isBuffer(compressed)) return row
|
|
996
|
-
|
|
997
|
-
try {
|
|
998
|
-
const bufferString = await decompressZstdOrInflateToString(compressed)
|
|
999
|
-
const properties = JSON.parse(bufferString)
|
|
1000
|
-
;(row as any).__compressed = undefined
|
|
1001
|
-
;(row as any).data = undefined
|
|
1002
|
-
Object.assign(row, properties)
|
|
1003
|
-
} catch {
|
|
1004
|
-
// Decompression failed - field is not compressed, leave as-is
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
return row
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
/**
|
|
1011
|
-
* Temporary helper to migrate from the old `data` compressed property to the new `__compressed` property.
|
|
1012
|
-
* Use as your `beforeDBMToBM` hook during the migration period.
|
|
1013
|
-
*
|
|
1014
|
-
* Migration steps:
|
|
1015
|
-
* 1. Add `beforeDBMToBM: CommonDao.migrateCompressedDataProperty` to your hooks
|
|
1016
|
-
* 2. Deploy - old data (with `data` property) will be decompressed on read and recompressed to `__compressed` on write
|
|
1017
|
-
* 3. Once all data has been naturally rewritten, remove the hook
|
|
1018
|
-
*
|
|
1019
|
-
* @example
|
|
1020
|
-
* const dao = new CommonDao({
|
|
1021
|
-
* compress: { keys: ['field1', 'field2'] },
|
|
1022
|
-
* hooks: {
|
|
1023
|
-
* beforeDBMToBM: CommonDao.migrateCompressedDataProperty,
|
|
1024
|
-
* }
|
|
1025
|
-
* })
|
|
1026
|
-
*/
|
|
1027
|
-
static async migrateCompressedDataProperty<T extends ObjectWithId>(row: T): Promise<T> {
|
|
1028
|
-
const data = (row as any).data
|
|
1029
|
-
if (!Buffer.isBuffer(data)) return row
|
|
1030
|
-
|
|
1031
|
-
try {
|
|
1032
|
-
const bufferString = await decompressZstdOrInflateToString(data)
|
|
1033
|
-
const properties = JSON.parse(bufferString)
|
|
1034
|
-
;(row as any).data = undefined
|
|
1035
|
-
Object.assign(row, properties)
|
|
1036
|
-
} catch {
|
|
1037
|
-
// Decompression failed - data field is not compressed, leave as-is
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
return row
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
972
|
/**
|
|
1044
973
|
* Load rows (by their ids) from Multiple tables at once.
|
|
1045
974
|
* An optimized way to load data, minimizing DB round-trips.
|
|
@@ -1128,7 +1057,7 @@ export class CommonDao<
|
|
|
1128
1057
|
const row = dbmByTableById[table]![input.id]
|
|
1129
1058
|
// Decompress before converting to BM
|
|
1130
1059
|
const dbm = row ? await dao.storageRowToDBM(row) : undefined
|
|
1131
|
-
bmsByProp[prop] =
|
|
1060
|
+
bmsByProp[prop] = dao.dbmToBM(dbm, opt) || null
|
|
1132
1061
|
} else {
|
|
1133
1062
|
// Plural
|
|
1134
1063
|
// We apply filtering, to be able to support multiple input props fetching from the same table.
|
|
@@ -1136,7 +1065,7 @@ export class CommonDao<
|
|
|
1136
1065
|
const rows = input.ids.map(id => dbmByTableById[table]![id]).filter(_isTruthy)
|
|
1137
1066
|
// Decompress before converting to BM
|
|
1138
1067
|
const dbms = await dao.storageRowsToDBMs(rows)
|
|
1139
|
-
bmsByProp[prop] =
|
|
1068
|
+
bmsByProp[prop] = dao.dbmsToBM(dbms, opt)
|
|
1140
1069
|
}
|
|
1141
1070
|
})
|
|
1142
1071
|
|
|
@@ -1198,14 +1127,14 @@ export class CommonDao<
|
|
|
1198
1127
|
}
|
|
1199
1128
|
|
|
1200
1129
|
dao.assignIdCreatedUpdated(row, opt)
|
|
1201
|
-
const dbm =
|
|
1130
|
+
const dbm = dao.bmToDBM(row, opt)
|
|
1202
1131
|
dao.cfg.hooks!.beforeSave?.(dbm)
|
|
1203
1132
|
const storageRow = await dao.dbmToStorageRow(dbm)
|
|
1204
1133
|
dbmsByTable[table].push(storageRow)
|
|
1205
1134
|
} else {
|
|
1206
1135
|
// Plural
|
|
1207
1136
|
input.rows.forEach(bm => dao.assignIdCreatedUpdated(bm, opt))
|
|
1208
|
-
const dbms =
|
|
1137
|
+
const dbms = dao.bmsToDBM(input.rows, opt)
|
|
1209
1138
|
if (dao.cfg.hooks!.beforeSave) {
|
|
1210
1139
|
dbms.forEach(dbm => dao.cfg.hooks!.beforeSave!(dbm))
|
|
1211
1140
|
}
|