@naturalcycles/db-lib 10.42.0 → 10.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/file/file.db.d.ts +1 -1
- package/dist/adapter/file/file.db.js +1 -1
- package/dist/commondao/common.dao.d.ts +73 -5
- package/dist/commondao/common.dao.js +189 -53
- package/dist/commondao/common.dao.model.d.ts +6 -5
- package/dist/inmemory/inMemory.db.d.ts +1 -1
- package/dist/inmemory/inMemory.db.js +1 -1
- package/dist/kv/commonKeyValueDao.d.ts +1 -1
- package/dist/pipeline/dbPipelineRestore.d.ts +2 -1
- package/dist/pipeline/dbPipelineRestore.js +1 -1
- package/dist/testing/test.model.d.ts +1 -1
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +2 -2
- package/package.json +2 -1
- package/src/adapter/file/file.db.ts +2 -5
- package/src/commondao/common.dao.model.ts +6 -5
- package/src/commondao/common.dao.ts +208 -68
- package/src/inmemory/inMemory.db.ts +2 -7
- package/src/kv/commonKeyValueDao.ts +2 -1
- package/src/pipeline/dbPipelineRestore.ts +4 -5
- package/src/testing/test.model.ts +2 -1
- package/src/validation/index.ts +4 -10
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ObjectWithId } from '@naturalcycles/js-lib/types';
|
|
2
2
|
import type { JsonSchema } from '@naturalcycles/nodejs-lib/ajv';
|
|
3
3
|
import { Pipeline } from '@naturalcycles/nodejs-lib/stream';
|
|
4
4
|
import { BaseCommonDB } from '../../commondb/base.common.db.js';
|
|
@@ -3,7 +3,7 @@ import { _by, _sortBy } from '@naturalcycles/js-lib/array';
|
|
|
3
3
|
import { _since, localTime } from '@naturalcycles/js-lib/datetime';
|
|
4
4
|
import { _assert } from '@naturalcycles/js-lib/error/assert.js';
|
|
5
5
|
import { _deepEquals, _filterUndefinedValues, _sortObjectDeep } from '@naturalcycles/js-lib/object';
|
|
6
|
-
import { _stringMapValues
|
|
6
|
+
import { _stringMapValues } from '@naturalcycles/js-lib/types';
|
|
7
7
|
import { generateJsonSchemaFromData } from '@naturalcycles/nodejs-lib/ajv';
|
|
8
8
|
import { dimGrey } from '@naturalcycles/nodejs-lib/colors';
|
|
9
9
|
import { Pipeline } from '@naturalcycles/nodejs-lib/stream';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { BaseDBEntity, NonNegativeInteger, ObjectWithId, StringMap, Unsaved } from '@naturalcycles/js-lib/types';
|
|
2
2
|
import type { JsonSchema } from '@naturalcycles/nodejs-lib/ajv';
|
|
3
3
|
import type { Pipeline } from '@naturalcycles/nodejs-lib/stream';
|
|
4
4
|
import type { CommonDBTransactionOptions, RunQueryResult } from '../db.model.js';
|
|
@@ -9,9 +9,12 @@ import { CommonDaoTransaction } from './commonDaoTransaction.js';
|
|
|
9
9
|
/**
|
|
10
10
|
* Lowest common denominator API between supported Databases.
|
|
11
11
|
*
|
|
12
|
-
* DBM = Database model (how it's stored in DB)
|
|
13
12
|
* BM = Backend model (optimized for API access)
|
|
13
|
+
* DBM = Database model (logical representation, before compression)
|
|
14
14
|
* TM = Transport model (optimized to be sent over the wire)
|
|
15
|
+
*
|
|
16
|
+
* Note: When auto-compression is enabled, the physical storage format differs from DBM.
|
|
17
|
+
* Compression/decompression is handled transparently at the storage boundary.
|
|
15
18
|
*/
|
|
16
19
|
export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM, ID extends string = BM['id']> {
|
|
17
20
|
cfg: CommonDaoCfg<BM, DBM, ID>;
|
|
@@ -46,7 +49,6 @@ export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity
|
|
|
46
49
|
runQueryCount(q: DBQuery<DBM>, opt?: CommonDaoReadOptions): Promise<number>;
|
|
47
50
|
streamQueryAsDBM(q: DBQuery<DBM>, opt?: CommonDaoStreamOptions<DBM>): Pipeline<DBM>;
|
|
48
51
|
streamQuery(q: DBQuery<DBM>, opt?: CommonDaoStreamOptions<BM>): Pipeline<BM>;
|
|
49
|
-
private streamQueryRaw;
|
|
50
52
|
queryIds(q: DBQuery<DBM>, opt?: CommonDaoReadOptions): Promise<ID[]>;
|
|
51
53
|
streamQueryIds(q: DBQuery<DBM>, opt?: CommonDaoStreamOptions<ID>): Pipeline<ID>;
|
|
52
54
|
/**
|
|
@@ -131,14 +133,43 @@ export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity
|
|
|
131
133
|
bmToDBM(bm: undefined, opt?: CommonDaoOptions): Promise<null>;
|
|
132
134
|
bmToDBM(bm?: BM, opt?: CommonDaoOptions): Promise<DBM>;
|
|
133
135
|
bmsToDBM(bms: BM[], opt?: CommonDaoOptions): Promise<DBM[]>;
|
|
136
|
+
/**
|
|
137
|
+
* Converts a DBM to storage format, applying compression if configured.
|
|
138
|
+
*
|
|
139
|
+
* Use this when you need to write directly to the database, bypassing the DAO save methods.
|
|
140
|
+
* The returned value is opaque and should only be passed to db.saveBatch() or similar.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* const storageRow = await dao.dbmToStorageRow(dbm)
|
|
144
|
+
* await db.saveBatch(table, [storageRow])
|
|
145
|
+
*/
|
|
146
|
+
dbmToStorageRow(dbm: DBM): Promise<ObjectWithId>;
|
|
147
|
+
/**
|
|
148
|
+
* Converts multiple DBMs to storage rows.
|
|
149
|
+
*/
|
|
150
|
+
dbmsToStorageRows(dbms: DBM[]): Promise<ObjectWithId[]>;
|
|
151
|
+
/**
|
|
152
|
+
* Converts a storage row back to a DBM, applying decompression if needed.
|
|
153
|
+
*
|
|
154
|
+
* Use this when you need to read directly from the database, bypassing the DAO load methods.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* const rows = await db.getByIds(table, ids)
|
|
158
|
+
* const dbms = await Promise.all(rows.map(row => dao.storageRowToDBM(row)))
|
|
159
|
+
*/
|
|
160
|
+
storageRowToDBM(row: ObjectWithId): Promise<DBM>;
|
|
161
|
+
/**
|
|
162
|
+
* Converts multiple storage rows to DBMs.
|
|
163
|
+
*/
|
|
164
|
+
storageRowsToDBMs(rows: ObjectWithId[]): Promise<DBM[]>;
|
|
134
165
|
/**
|
|
135
166
|
* Mutates `dbm`.
|
|
136
167
|
*/
|
|
137
|
-
compress
|
|
168
|
+
private compress;
|
|
138
169
|
/**
|
|
139
170
|
* Mutates `dbm`.
|
|
140
171
|
*/
|
|
141
|
-
decompress
|
|
172
|
+
private decompress;
|
|
142
173
|
anyToDBM(dbm: undefined, opt?: CommonDaoOptions): Promise<null>;
|
|
143
174
|
anyToDBM(dbm?: any, opt?: CommonDaoOptions): Promise<DBM>;
|
|
144
175
|
anyToDBMs(rows: DBM[], opt?: CommonDaoOptions): Promise<DBM[]>;
|
|
@@ -158,6 +189,43 @@ export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity
|
|
|
158
189
|
withIds(ids: ID[]): DaoWithIds<CommonDao<BM, DBM, ID>>;
|
|
159
190
|
withRowsToSave(rows: Unsaved<BM>[]): DaoWithRows<CommonDao<BM, DBM, ID>>;
|
|
160
191
|
withRowToSave(row: Unsaved<BM>, opt?: DaoWithRowOptions<BM>): DaoWithRow<CommonDao<BM, DBM, ID>>;
|
|
192
|
+
/**
|
|
193
|
+
* Helper to decompress legacy compressed data when migrating away from auto-compression.
|
|
194
|
+
* Use as your `beforeDBMToBM` hook to decompress legacy rows on read.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* const dao = new CommonDao({
|
|
198
|
+
* hooks: {
|
|
199
|
+
* beforeDBMToBM: CommonDao.decompressLegacyRow,
|
|
200
|
+
* }
|
|
201
|
+
* })
|
|
202
|
+
*
|
|
203
|
+
* // Or within an existing hook:
|
|
204
|
+
* beforeDBMToBM: async (dbm) => {
|
|
205
|
+
* await CommonDao.decompressLegacyRow(dbm)
|
|
206
|
+
* // ... other transformations
|
|
207
|
+
* return dbm
|
|
208
|
+
* }
|
|
209
|
+
*/
|
|
210
|
+
static decompressLegacyRow<T extends ObjectWithId>(row: T): Promise<T>;
|
|
211
|
+
/**
|
|
212
|
+
* Temporary helper to migrate from the old `data` compressed property to the new `__compressed` property.
|
|
213
|
+
* Use as your `beforeDBMToBM` hook during the migration period.
|
|
214
|
+
*
|
|
215
|
+
* Migration steps:
|
|
216
|
+
* 1. Add `beforeDBMToBM: CommonDao.migrateCompressedDataProperty` to your hooks
|
|
217
|
+
* 2. Deploy - old data (with `data` property) will be decompressed on read and recompressed to `__compressed` on write
|
|
218
|
+
* 3. Once all data has been naturally rewritten, remove the hook
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* const dao = new CommonDao({
|
|
222
|
+
* compress: { keys: ['field1', 'field2'] },
|
|
223
|
+
* hooks: {
|
|
224
|
+
* beforeDBMToBM: CommonDao.migrateCompressedDataProperty,
|
|
225
|
+
* }
|
|
226
|
+
* })
|
|
227
|
+
*/
|
|
228
|
+
static migrateCompressedDataProperty<T extends ObjectWithId>(row: T): Promise<T>;
|
|
161
229
|
/**
|
|
162
230
|
* Load rows (by their ids) from Multiple tables at once.
|
|
163
231
|
* An optimized way to load data, minimizing DB round-trips.
|
|
@@ -14,9 +14,12 @@ import { CommonDaoTransaction } from './commonDaoTransaction.js';
|
|
|
14
14
|
/**
|
|
15
15
|
* Lowest common denominator API between supported Databases.
|
|
16
16
|
*
|
|
17
|
-
* DBM = Database model (how it's stored in DB)
|
|
18
17
|
* BM = Backend model (optimized for API access)
|
|
18
|
+
* DBM = Database model (logical representation, before compression)
|
|
19
19
|
* TM = Transport model (optimized to be sent over the wire)
|
|
20
|
+
*
|
|
21
|
+
* Note: When auto-compression is enabled, the physical storage format differs from DBM.
|
|
22
|
+
* Compression/decompression is handled transparently at the storage boundary.
|
|
20
23
|
*/
|
|
21
24
|
export class CommonDao {
|
|
22
25
|
cfg;
|
|
@@ -44,6 +47,15 @@ export class CommonDao {
|
|
|
44
47
|
else {
|
|
45
48
|
delete this.cfg.hooks.createRandomId;
|
|
46
49
|
}
|
|
50
|
+
// If the auto-compression is enabled,
|
|
51
|
+
// then we need to ensure that the '__compressed' property is part of the index exclusion list.
|
|
52
|
+
if (this.cfg.compress?.keys) {
|
|
53
|
+
const current = this.cfg.excludeFromIndexes;
|
|
54
|
+
this.cfg.excludeFromIndexes = current ? [...current] : [];
|
|
55
|
+
if (!this.cfg.excludeFromIndexes.includes('__compressed')) {
|
|
56
|
+
this.cfg.excludeFromIndexes.push('__compressed');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
47
59
|
}
|
|
48
60
|
// CREATE
|
|
49
61
|
create(part = {}, opt = {}) {
|
|
@@ -92,7 +104,8 @@ export class CommonDao {
|
|
|
92
104
|
if (!ids.length)
|
|
93
105
|
return [];
|
|
94
106
|
const table = opt.table || this.cfg.table;
|
|
95
|
-
|
|
107
|
+
const rows = await (opt.tx || this.cfg.db).getByIds(table, ids, opt);
|
|
108
|
+
return await this.storageRowsToDBMs(rows);
|
|
96
109
|
}
|
|
97
110
|
async getBy(by, value, limit = 0, opt) {
|
|
98
111
|
return await this.query().filterEq(by, value).limit(limit).runQuery(opt);
|
|
@@ -133,8 +146,9 @@ export class CommonDao {
|
|
|
133
146
|
async runQueryExtended(q, opt = {}) {
|
|
134
147
|
this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
|
|
135
148
|
q.table = opt.table || q.table;
|
|
136
|
-
const { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
|
|
149
|
+
const { rows: rawRows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
|
|
137
150
|
const isPartialQuery = !!q._selectedFieldNames;
|
|
151
|
+
const rows = isPartialQuery ? rawRows : await this.storageRowsToDBMs(rawRows);
|
|
138
152
|
const bms = isPartialQuery ? rows : await this.dbmsToBM(rows, opt);
|
|
139
153
|
return {
|
|
140
154
|
rows: bms,
|
|
@@ -148,8 +162,9 @@ export class CommonDao {
|
|
|
148
162
|
async runQueryExtendedAsDBM(q, opt = {}) {
|
|
149
163
|
this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
|
|
150
164
|
q.table = opt.table || q.table;
|
|
151
|
-
const { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
|
|
165
|
+
const { rows: rawRows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
|
|
152
166
|
const isPartialQuery = !!q._selectedFieldNames;
|
|
167
|
+
const rows = isPartialQuery ? rawRows : await this.storageRowsToDBMs(rawRows);
|
|
153
168
|
const dbms = isPartialQuery ? rows : await this.anyToDBMs(rows, opt);
|
|
154
169
|
return { rows: dbms, ...queryResult };
|
|
155
170
|
}
|
|
@@ -159,7 +174,12 @@ export class CommonDao {
|
|
|
159
174
|
return await this.cfg.db.runQueryCount(q, opt);
|
|
160
175
|
}
|
|
161
176
|
streamQueryAsDBM(q, opt = {}) {
|
|
162
|
-
|
|
177
|
+
this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
|
|
178
|
+
q.table = opt.table || q.table;
|
|
179
|
+
let pipeline = this.cfg.db.streamQuery(q, opt);
|
|
180
|
+
if (this.cfg.compress?.keys.length) {
|
|
181
|
+
pipeline = pipeline.map(async (row) => await this.storageRowToDBM(row));
|
|
182
|
+
}
|
|
163
183
|
const isPartialQuery = !!q._selectedFieldNames;
|
|
164
184
|
if (isPartialQuery)
|
|
165
185
|
return pipeline;
|
|
@@ -168,7 +188,12 @@ export class CommonDao {
|
|
|
168
188
|
return pipeline.map(async (dbm) => await this.anyToDBM(dbm, opt), { errorMode: opt.errorMode });
|
|
169
189
|
}
|
|
170
190
|
streamQuery(q, opt = {}) {
|
|
171
|
-
|
|
191
|
+
this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
|
|
192
|
+
q.table = opt.table || q.table;
|
|
193
|
+
let pipeline = this.cfg.db.streamQuery(q, opt);
|
|
194
|
+
if (this.cfg.compress?.keys.length) {
|
|
195
|
+
pipeline = pipeline.map(async (row) => await this.storageRowToDBM(row));
|
|
196
|
+
}
|
|
172
197
|
const isPartialQuery = !!q._selectedFieldNames;
|
|
173
198
|
if (isPartialQuery)
|
|
174
199
|
return pipeline;
|
|
@@ -176,11 +201,6 @@ export class CommonDao {
|
|
|
176
201
|
opt.errorMode ||= ErrorMode.SUPPRESS;
|
|
177
202
|
return pipeline.map(async (dbm) => await this.dbmToBM(dbm, opt), { errorMode: opt.errorMode });
|
|
178
203
|
}
|
|
179
|
-
streamQueryRaw(q, opt = {}) {
|
|
180
|
-
this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
|
|
181
|
-
q.table = opt.table || q.table;
|
|
182
|
-
return this.cfg.db.streamQuery(q, opt);
|
|
183
|
-
}
|
|
184
204
|
async queryIds(q, opt = {}) {
|
|
185
205
|
this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
|
|
186
206
|
q.table = opt.table || q.table;
|
|
@@ -331,7 +351,8 @@ export class CommonDao {
|
|
|
331
351
|
this.cfg.hooks.beforeSave?.(dbm);
|
|
332
352
|
const table = opt.table || this.cfg.table;
|
|
333
353
|
const saveOptions = this.prepareSaveOptions(opt);
|
|
334
|
-
|
|
354
|
+
const row = await this.dbmToStorageRow(dbm);
|
|
355
|
+
await (opt.tx || this.cfg.db).saveBatch(table, [row], saveOptions);
|
|
335
356
|
if (saveOptions.assignGeneratedIds) {
|
|
336
357
|
bm.id = dbm.id;
|
|
337
358
|
}
|
|
@@ -340,15 +361,16 @@ export class CommonDao {
|
|
|
340
361
|
async saveAsDBM(dbm, opt = {}) {
|
|
341
362
|
this.requireWriteAccess();
|
|
342
363
|
this.assignIdCreatedUpdated(dbm, opt); // mutates
|
|
343
|
-
const
|
|
344
|
-
this.cfg.hooks.beforeSave?.(
|
|
364
|
+
const validDbm = await this.anyToDBM(dbm, opt);
|
|
365
|
+
this.cfg.hooks.beforeSave?.(validDbm);
|
|
345
366
|
const table = opt.table || this.cfg.table;
|
|
346
367
|
const saveOptions = this.prepareSaveOptions(opt);
|
|
368
|
+
const row = await this.dbmToStorageRow(validDbm);
|
|
347
369
|
await (opt.tx || this.cfg.db).saveBatch(table, [row], saveOptions);
|
|
348
370
|
if (saveOptions.assignGeneratedIds) {
|
|
349
|
-
dbm.id =
|
|
371
|
+
dbm.id = validDbm.id;
|
|
350
372
|
}
|
|
351
|
-
return
|
|
373
|
+
return validDbm;
|
|
352
374
|
}
|
|
353
375
|
async saveBatch(bms, opt = {}) {
|
|
354
376
|
if (!bms.length)
|
|
@@ -361,7 +383,8 @@ export class CommonDao {
|
|
|
361
383
|
}
|
|
362
384
|
const table = opt.table || this.cfg.table;
|
|
363
385
|
const saveOptions = this.prepareSaveOptions(opt);
|
|
364
|
-
|
|
386
|
+
const rows = await this.dbmsToStorageRows(dbms);
|
|
387
|
+
await (opt.tx || this.cfg.db).saveBatch(table, rows, saveOptions);
|
|
365
388
|
if (saveOptions.assignGeneratedIds) {
|
|
366
389
|
dbms.forEach((dbm, i) => (bms[i].id = dbm.id));
|
|
367
390
|
}
|
|
@@ -372,24 +395,28 @@ export class CommonDao {
|
|
|
372
395
|
return [];
|
|
373
396
|
this.requireWriteAccess();
|
|
374
397
|
dbms.forEach(dbm => this.assignIdCreatedUpdated(dbm, opt));
|
|
375
|
-
const
|
|
398
|
+
const validDbms = await this.anyToDBMs(dbms, opt);
|
|
376
399
|
if (this.cfg.hooks.beforeSave) {
|
|
377
|
-
|
|
400
|
+
validDbms.forEach(dbm => this.cfg.hooks.beforeSave(dbm));
|
|
378
401
|
}
|
|
379
402
|
const table = opt.table || this.cfg.table;
|
|
380
403
|
const saveOptions = this.prepareSaveOptions(opt);
|
|
404
|
+
const rows = await this.dbmsToStorageRows(validDbms);
|
|
381
405
|
await (opt.tx || this.cfg.db).saveBatch(table, rows, saveOptions);
|
|
382
406
|
if (saveOptions.assignGeneratedIds) {
|
|
383
|
-
|
|
407
|
+
validDbms.forEach((dbm, i) => (dbms[i].id = dbm.id));
|
|
384
408
|
}
|
|
385
|
-
return
|
|
409
|
+
return validDbms;
|
|
386
410
|
}
|
|
387
411
|
prepareSaveOptions(opt) {
|
|
388
412
|
let { saveMethod, assignGeneratedIds = this.cfg.assignGeneratedIds, excludeFromIndexes = this.cfg.excludeFromIndexes, } = opt;
|
|
413
|
+
// If the user passed in custom `excludeFromIndexes` with the save() call,
|
|
414
|
+
// and the auto-compression is enabled,
|
|
415
|
+
// then we need to ensure that the '__compressed' property is part of the list.
|
|
389
416
|
if (this.cfg.compress?.keys) {
|
|
390
417
|
excludeFromIndexes ??= [];
|
|
391
|
-
if (!excludeFromIndexes.includes('
|
|
392
|
-
excludeFromIndexes.push('
|
|
418
|
+
if (!excludeFromIndexes.includes('__compressed')) {
|
|
419
|
+
excludeFromIndexes.push('__compressed');
|
|
393
420
|
}
|
|
394
421
|
}
|
|
395
422
|
if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
|
|
@@ -397,7 +424,7 @@ export class CommonDao {
|
|
|
397
424
|
}
|
|
398
425
|
return {
|
|
399
426
|
...opt,
|
|
400
|
-
excludeFromIndexes,
|
|
427
|
+
excludeFromIndexes: excludeFromIndexes,
|
|
401
428
|
saveMethod,
|
|
402
429
|
assignGeneratedIds,
|
|
403
430
|
};
|
|
@@ -416,10 +443,7 @@ export class CommonDao {
|
|
|
416
443
|
const table = opt.table || this.cfg.table;
|
|
417
444
|
opt.skipValidation ??= true;
|
|
418
445
|
opt.errorMode ||= ErrorMode.SUPPRESS;
|
|
419
|
-
|
|
420
|
-
opt = { ...opt, saveMethod: 'insert' };
|
|
421
|
-
}
|
|
422
|
-
const excludeFromIndexes = opt.excludeFromIndexes || this.cfg.excludeFromIndexes;
|
|
446
|
+
const saveOptions = this.prepareSaveOptions(opt);
|
|
423
447
|
const { beforeSave } = this.cfg.hooks;
|
|
424
448
|
const { chunkSize = 500, chunkConcurrency = 32, errorMode } = opt;
|
|
425
449
|
await p
|
|
@@ -427,14 +451,11 @@ export class CommonDao {
|
|
|
427
451
|
this.assignIdCreatedUpdated(bm, opt);
|
|
428
452
|
const dbm = await this.bmToDBM(bm, opt);
|
|
429
453
|
beforeSave?.(dbm);
|
|
430
|
-
return dbm;
|
|
454
|
+
return await this.dbmToStorageRow(dbm);
|
|
431
455
|
}, { errorMode })
|
|
432
456
|
.chunk(chunkSize)
|
|
433
457
|
.map(async (batch) => {
|
|
434
|
-
await this.cfg.db.saveBatch(table, batch,
|
|
435
|
-
...opt,
|
|
436
|
-
excludeFromIndexes,
|
|
437
|
-
});
|
|
458
|
+
await this.cfg.db.saveBatch(table, batch, saveOptions);
|
|
438
459
|
return batch;
|
|
439
460
|
}, {
|
|
440
461
|
concurrency: chunkConcurrency,
|
|
@@ -545,8 +566,6 @@ export class CommonDao {
|
|
|
545
566
|
// optimization: no need to run full joi DBM validation, cause BM validation will be run
|
|
546
567
|
// const dbm = this.anyToDBM(_dbm, opt)
|
|
547
568
|
const dbm = { ..._dbm, ...this.cfg.hooks.parseNaturalId(_dbm.id) };
|
|
548
|
-
// Decompress
|
|
549
|
-
await this.decompress(dbm);
|
|
550
569
|
// DBM > BM
|
|
551
570
|
const bm = ((await this.cfg.hooks.beforeDBMToBM?.(dbm)) || dbm);
|
|
552
571
|
// Validate/convert BM
|
|
@@ -562,15 +581,65 @@ export class CommonDao {
|
|
|
562
581
|
bm = this.validateAndConvert(bm, 'save', opt);
|
|
563
582
|
// BM > DBM
|
|
564
583
|
const dbm = ((await this.cfg.hooks.beforeBMToDBM?.(bm)) || bm);
|
|
565
|
-
// Compress
|
|
566
|
-
if (this.cfg.compress)
|
|
567
|
-
await this.compress(dbm);
|
|
568
584
|
return dbm;
|
|
569
585
|
}
|
|
570
586
|
async bmsToDBM(bms, opt = {}) {
|
|
571
587
|
// try/catch?
|
|
572
588
|
return await pMap(bms, async (bm) => await this.bmToDBM(bm, opt));
|
|
573
589
|
}
|
|
590
|
+
// STORAGE LAYER (compression/decompression at DB boundary)
|
|
591
|
+
// These methods convert between DBM (logical model) and storage format (physical, possibly compressed).
|
|
592
|
+
// Public methods allow external code to bypass the DAO layer for direct DB access
|
|
593
|
+
// (e.g., cross-environment data copy).
|
|
594
|
+
/**
|
|
595
|
+
* Converts a DBM to storage format, applying compression if configured.
|
|
596
|
+
*
|
|
597
|
+
* Use this when you need to write directly to the database, bypassing the DAO save methods.
|
|
598
|
+
* The returned value is opaque and should only be passed to db.saveBatch() or similar.
|
|
599
|
+
*
|
|
600
|
+
* @example
|
|
601
|
+
* const storageRow = await dao.dbmToStorageRow(dbm)
|
|
602
|
+
* await db.saveBatch(table, [storageRow])
|
|
603
|
+
*/
|
|
604
|
+
async dbmToStorageRow(dbm) {
|
|
605
|
+
if (!this.cfg.compress?.keys.length)
|
|
606
|
+
return dbm;
|
|
607
|
+
const row = { ...dbm };
|
|
608
|
+
await this.compress(row);
|
|
609
|
+
return row;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Converts multiple DBMs to storage rows.
|
|
613
|
+
*/
|
|
614
|
+
async dbmsToStorageRows(dbms) {
|
|
615
|
+
if (!this.cfg.compress?.keys.length)
|
|
616
|
+
return dbms;
|
|
617
|
+
return await pMap(dbms, async (dbm) => await this.dbmToStorageRow(dbm));
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Converts a storage row back to a DBM, applying decompression if needed.
|
|
621
|
+
*
|
|
622
|
+
* Use this when you need to read directly from the database, bypassing the DAO load methods.
|
|
623
|
+
*
|
|
624
|
+
* @example
|
|
625
|
+
* const rows = await db.getByIds(table, ids)
|
|
626
|
+
* const dbms = await Promise.all(rows.map(row => dao.storageRowToDBM(row)))
|
|
627
|
+
*/
|
|
628
|
+
async storageRowToDBM(row) {
|
|
629
|
+
if (!this.cfg.compress?.keys.length)
|
|
630
|
+
return row;
|
|
631
|
+
const dbm = { ...row };
|
|
632
|
+
await this.decompress(dbm);
|
|
633
|
+
return dbm;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Converts multiple storage rows to DBMs.
|
|
637
|
+
*/
|
|
638
|
+
async storageRowsToDBMs(rows) {
|
|
639
|
+
if (!this.cfg.compress?.keys.length)
|
|
640
|
+
return rows;
|
|
641
|
+
return await pMap(rows, async (row) => await this.storageRowToDBM(row));
|
|
642
|
+
}
|
|
574
643
|
/**
|
|
575
644
|
* Mutates `dbm`.
|
|
576
645
|
*/
|
|
@@ -579,26 +648,22 @@ export class CommonDao {
|
|
|
579
648
|
return; // No compression requested
|
|
580
649
|
const { keys } = this.cfg.compress;
|
|
581
650
|
const properties = _pick(dbm, keys);
|
|
582
|
-
_assert(!('data' in dbm) || 'data' in properties, `Data (${dbm.id}) already has a "data" property. When using compression, this property must be included in the compression keys list.`);
|
|
583
651
|
const bufferString = JSON.stringify(properties);
|
|
584
|
-
const
|
|
652
|
+
const __compressed = await zstdCompress(bufferString);
|
|
585
653
|
_omitWithUndefined(dbm, _objectKeys(properties), { mutate: true });
|
|
586
|
-
Object.assign(dbm, {
|
|
654
|
+
Object.assign(dbm, { __compressed });
|
|
587
655
|
}
|
|
588
656
|
/**
|
|
589
657
|
* Mutates `dbm`.
|
|
590
658
|
*/
|
|
591
659
|
async decompress(dbm) {
|
|
592
660
|
_typeCast(dbm);
|
|
593
|
-
if (!
|
|
594
|
-
return; // Auto-compression not turned on
|
|
595
|
-
if (!Buffer.isBuffer(dbm.data))
|
|
661
|
+
if (!Buffer.isBuffer(dbm.__compressed))
|
|
596
662
|
return; // No compressed data
|
|
597
|
-
// try-catch to avoid a `data` with Buffer which is not compressed, but legit data
|
|
598
663
|
try {
|
|
599
|
-
const bufferString = await decompressZstdOrInflateToString(dbm.
|
|
664
|
+
const bufferString = await decompressZstdOrInflateToString(dbm.__compressed);
|
|
600
665
|
const properties = JSON.parse(bufferString);
|
|
601
|
-
dbm.
|
|
666
|
+
dbm.__compressed = undefined;
|
|
602
667
|
Object.assign(dbm, properties);
|
|
603
668
|
}
|
|
604
669
|
catch { }
|
|
@@ -609,8 +674,6 @@ export class CommonDao {
|
|
|
609
674
|
// this shouldn't be happening on load! but should on save!
|
|
610
675
|
// this.assignIdCreatedUpdated(dbm, opt)
|
|
611
676
|
dbm = { ...dbm, ...this.cfg.hooks.parseNaturalId(dbm.id) };
|
|
612
|
-
// Decompress
|
|
613
|
-
await this.decompress(dbm);
|
|
614
677
|
// Validate/convert DBM
|
|
615
678
|
// return this.validateAndConvert(dbm, this.cfg.dbmSchema, DBModelType.DBM, opt)
|
|
616
679
|
return dbm;
|
|
@@ -688,6 +751,73 @@ export class CommonDao {
|
|
|
688
751
|
opt: opt,
|
|
689
752
|
};
|
|
690
753
|
}
|
|
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
|
+
}
|
|
691
821
|
/**
|
|
692
822
|
* Load rows (by their ids) from Multiple tables at once.
|
|
693
823
|
* An optimized way to load data, minimizing DB round-trips.
|
|
@@ -748,14 +878,18 @@ export class CommonDao {
|
|
|
748
878
|
const { table } = dao.cfg;
|
|
749
879
|
if ('id' in input) {
|
|
750
880
|
// Singular
|
|
751
|
-
const
|
|
881
|
+
const row = dbmByTableById[table][input.id];
|
|
882
|
+
// Decompress before converting to BM
|
|
883
|
+
const dbm = row ? await dao.storageRowToDBM(row) : undefined;
|
|
752
884
|
bmsByProp[prop] = (await dao.dbmToBM(dbm, opt)) || null;
|
|
753
885
|
}
|
|
754
886
|
else {
|
|
755
887
|
// Plural
|
|
756
888
|
// We apply filtering, to be able to support multiple input props fetching from the same table.
|
|
757
889
|
// Without filtering - every prop will get ALL rows from that table.
|
|
758
|
-
const
|
|
890
|
+
const rows = input.ids.map(id => dbmByTableById[table][id]).filter(_isTruthy);
|
|
891
|
+
// Decompress before converting to BM
|
|
892
|
+
const dbms = await dao.storageRowsToDBMs(rows);
|
|
759
893
|
bmsByProp[prop] = await dao.dbmsToBM(dbms, opt);
|
|
760
894
|
}
|
|
761
895
|
});
|
|
@@ -809,7 +943,8 @@ export class CommonDao {
|
|
|
809
943
|
dao.assignIdCreatedUpdated(row, opt);
|
|
810
944
|
const dbm = await dao.bmToDBM(row, opt);
|
|
811
945
|
dao.cfg.hooks.beforeSave?.(dbm);
|
|
812
|
-
|
|
946
|
+
const storageRow = await dao.dbmToStorageRow(dbm);
|
|
947
|
+
dbmsByTable[table].push(storageRow);
|
|
813
948
|
}
|
|
814
949
|
else {
|
|
815
950
|
// Plural
|
|
@@ -818,7 +953,8 @@ export class CommonDao {
|
|
|
818
953
|
if (dao.cfg.hooks.beforeSave) {
|
|
819
954
|
dbms.forEach(dbm => dao.cfg.hooks.beforeSave(dbm));
|
|
820
955
|
}
|
|
821
|
-
|
|
956
|
+
const storageRows = await dao.dbmsToStorageRows(dbms);
|
|
957
|
+
dbmsByTable[table].push(...storageRows);
|
|
822
958
|
}
|
|
823
959
|
});
|
|
824
960
|
await db.multiSave(dbmsByTable);
|
|
@@ -168,14 +168,15 @@ export interface CommonDaoCfg<BM extends BaseDBEntity, DBM extends BaseDBEntity
|
|
|
168
168
|
*/
|
|
169
169
|
patchInTransaction?: boolean;
|
|
170
170
|
/**
|
|
171
|
-
* When specified, the listed properties will be compressed
|
|
172
|
-
* If DBM already has a `data` property and you don't add it to the list, an error will be thrown.
|
|
173
|
-
*
|
|
174
|
-
* When specified with an empty `keys` list, then compression will be skipped, but all previously compressed data
|
|
175
|
-
* will be decompressed, so the Dao can still work.
|
|
171
|
+
* When specified, the listed properties will be compressed into the `__compressed` property.
|
|
176
172
|
*
|
|
177
173
|
* Compression happens after the `beforeBMToDBM` hook and before the DBM is saved to the database.
|
|
178
174
|
* Decompression happens after the DBM is loaded from the database and before the `beforeDBMToBM` hook.
|
|
175
|
+
*
|
|
176
|
+
* To migrate away from compression:
|
|
177
|
+
* 1. Remove this config (or set `keys` to empty array)
|
|
178
|
+
* 2. Add `beforeDBMToBM: CommonDao.decompressLegacyRow` to your hooks to decompress legacy data on read
|
|
179
|
+
* 3. Once all data has been naturally rewritten without compression, remove the hook
|
|
179
180
|
*/
|
|
180
181
|
compress?: {
|
|
181
182
|
keys: (keyof DBM)[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CommonLogger } from '@naturalcycles/js-lib/log';
|
|
2
|
-
import {
|
|
2
|
+
import type { AnyObjectWithId, ObjectWithId, StringMap } from '@naturalcycles/js-lib/types';
|
|
3
3
|
import type { JsonSchema } from '@naturalcycles/nodejs-lib/ajv';
|
|
4
4
|
import { Pipeline } from '@naturalcycles/nodejs-lib/stream';
|
|
5
5
|
import type { CommonDB, CommonDBSupport } from '../commondb/common.db.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { _isEmptyObject } from '@naturalcycles/js-lib';
|
|
2
2
|
import { _assert } from '@naturalcycles/js-lib/error/assert.js';
|
|
3
3
|
import { _deepCopy, _sortObjectDeep } from '@naturalcycles/js-lib/object';
|
|
4
|
-
import { _stringMapEntries, _stringMapValues
|
|
4
|
+
import { _stringMapEntries, _stringMapValues } from '@naturalcycles/js-lib/types';
|
|
5
5
|
import { generateJsonSchemaFromData } from '@naturalcycles/nodejs-lib/ajv';
|
|
6
6
|
import { Pipeline } from '@naturalcycles/nodejs-lib/stream';
|
|
7
7
|
import { bufferReviver } from '@naturalcycles/nodejs-lib/stream/ndjson/transformJsonParse.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CommonLogger } from '@naturalcycles/js-lib/log';
|
|
2
|
-
import {
|
|
2
|
+
import type { Integer, KeyValueTuple } from '@naturalcycles/js-lib/types';
|
|
3
3
|
import type { Pipeline } from '@naturalcycles/nodejs-lib/stream';
|
|
4
4
|
import type { CommonDaoLogLevel } from '../commondao/common.dao.model.js';
|
|
5
5
|
import type { CommonDBCreateOptions } from '../db.model.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ErrorMode } from '@naturalcycles/js-lib/error/errorMode.js';
|
|
2
2
|
import type { AsyncMapper, UnixTimestamp } from '@naturalcycles/js-lib/types';
|
|
3
|
-
import { NDJsonStats
|
|
3
|
+
import { NDJsonStats } from '@naturalcycles/nodejs-lib/stream';
|
|
4
|
+
import type { TransformLogProgressOptions, TransformMapOptions } from '@naturalcycles/nodejs-lib/stream';
|
|
4
5
|
import type { CommonDB } from '../commondb/common.db.js';
|
|
5
6
|
import type { CommonDBSaveOptions } from '../db.model.js';
|
|
6
7
|
export interface DBPipelineRestoreOptions extends TransformLogProgressOptions {
|
|
@@ -6,7 +6,7 @@ import { pMap } from '@naturalcycles/js-lib/promise/pMap.js';
|
|
|
6
6
|
import { _passthroughMapper } from '@naturalcycles/js-lib/types';
|
|
7
7
|
import { boldWhite, dimWhite, grey, yellow } from '@naturalcycles/nodejs-lib/colors';
|
|
8
8
|
import { fs2 } from '@naturalcycles/nodejs-lib/fs2';
|
|
9
|
-
import { NDJsonStats, Pipeline
|
|
9
|
+
import { NDJsonStats, Pipeline } from '@naturalcycles/nodejs-lib/stream';
|
|
10
10
|
/**
|
|
11
11
|
* Pipeline from NDJSON files in a folder (optionally gzipped) to CommonDB.
|
|
12
12
|
* Allows to define a mapper and a predicate to map/filter objects between input and output.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BaseDBEntity } from '@naturalcycles/js-lib/types';
|
|
2
|
-
import {
|
|
2
|
+
import type { JsonSchemaObjectBuilder } from '@naturalcycles/nodejs-lib/ajv';
|
|
3
3
|
export declare const TEST_TABLE = "TEST_TABLE";
|
|
4
4
|
export declare const TEST_TABLE_2 = "TEST_TABLE_2";
|
|
5
5
|
export interface TestItemBM extends BaseDBEntity {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ObjectWithId } from '@naturalcycles/js-lib/types';
|
|
2
|
-
import {
|
|
2
|
+
import type { JsonSchemaObjectBuilder } from '@naturalcycles/nodejs-lib/ajv';
|
|
3
3
|
import type { CommonDBOptions } from '../db.model.js';
|
|
4
4
|
export declare const commonDBOptionsSchema: () => JsonSchemaObjectBuilder<CommonDBOptions, CommonDBOptions>;
|
|
5
5
|
export declare const commonDBSaveOptionsSchema: <ROW extends ObjectWithId>() => any;
|
package/dist/validation/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { j, JsonSchemaAnyBuilder
|
|
2
|
-
import { dbQueryFilterOperatorValues
|
|
1
|
+
import { j, JsonSchemaAnyBuilder } from '@naturalcycles/nodejs-lib/ajv';
|
|
2
|
+
import { dbQueryFilterOperatorValues } from '../query/dbQuery.js';
|
|
3
3
|
// oxlint-disable typescript/explicit-function-return-type
|
|
4
4
|
// DBTransaction schema - validates presence without deep validation
|
|
5
5
|
const dbTransactionSchema = j.object.any().castAs();
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/db-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "10.42.
|
|
4
|
+
"version": "10.42.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-0",
|
|
10
11
|
"@naturalcycles/dev-lib": "18.4.2"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
@@ -3,11 +3,8 @@ import { _by, _sortBy } from '@naturalcycles/js-lib/array'
|
|
|
3
3
|
import { _since, localTime } from '@naturalcycles/js-lib/datetime'
|
|
4
4
|
import { _assert } from '@naturalcycles/js-lib/error/assert.js'
|
|
5
5
|
import { _deepEquals, _filterUndefinedValues, _sortObjectDeep } from '@naturalcycles/js-lib/object'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
type ObjectWithId,
|
|
9
|
-
type UnixTimestampMillis,
|
|
10
|
-
} from '@naturalcycles/js-lib/types'
|
|
6
|
+
import { _stringMapValues } from '@naturalcycles/js-lib/types'
|
|
7
|
+
import type { ObjectWithId, UnixTimestampMillis } from '@naturalcycles/js-lib/types'
|
|
11
8
|
import type { JsonSchema } from '@naturalcycles/nodejs-lib/ajv'
|
|
12
9
|
import { generateJsonSchemaFromData } from '@naturalcycles/nodejs-lib/ajv'
|
|
13
10
|
import { dimGrey } from '@naturalcycles/nodejs-lib/colors'
|
|
@@ -207,14 +207,15 @@ export interface CommonDaoCfg<
|
|
|
207
207
|
patchInTransaction?: boolean
|
|
208
208
|
|
|
209
209
|
/**
|
|
210
|
-
* When specified, the listed properties will be compressed
|
|
211
|
-
* If DBM already has a `data` property and you don't add it to the list, an error will be thrown.
|
|
212
|
-
*
|
|
213
|
-
* When specified with an empty `keys` list, then compression will be skipped, but all previously compressed data
|
|
214
|
-
* will be decompressed, so the Dao can still work.
|
|
210
|
+
* When specified, the listed properties will be compressed into the `__compressed` property.
|
|
215
211
|
*
|
|
216
212
|
* Compression happens after the `beforeBMToDBM` hook and before the DBM is saved to the database.
|
|
217
213
|
* Decompression happens after the DBM is loaded from the database and before the `beforeDBMToBM` hook.
|
|
214
|
+
*
|
|
215
|
+
* To migrate away from compression:
|
|
216
|
+
* 1. Remove this config (or set `keys` to empty array)
|
|
217
|
+
* 2. Add `beforeDBMToBM: CommonDao.decompressLegacyRow` to your hooks to decompress legacy data on read
|
|
218
|
+
* 3. Once all data has been naturally rewritten without compression, remove the hook
|
|
218
219
|
*/
|
|
219
220
|
compress?: {
|
|
220
221
|
keys: (keyof DBM)[]
|
|
@@ -16,11 +16,13 @@ import {
|
|
|
16
16
|
_stringMapEntries,
|
|
17
17
|
_stringMapValues,
|
|
18
18
|
_typeCast,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
} from '@naturalcycles/js-lib/types'
|
|
20
|
+
import type {
|
|
21
|
+
BaseDBEntity,
|
|
22
|
+
NonNegativeInteger,
|
|
23
|
+
ObjectWithId,
|
|
24
|
+
StringMap,
|
|
25
|
+
Unsaved,
|
|
24
26
|
} from '@naturalcycles/js-lib/types'
|
|
25
27
|
import { stringId } from '@naturalcycles/nodejs-lib'
|
|
26
28
|
import type { JsonSchema } from '@naturalcycles/nodejs-lib/ajv'
|
|
@@ -53,9 +55,12 @@ import { CommonDaoTransaction } from './commonDaoTransaction.js'
|
|
|
53
55
|
/**
|
|
54
56
|
* Lowest common denominator API between supported Databases.
|
|
55
57
|
*
|
|
56
|
-
* DBM = Database model (how it's stored in DB)
|
|
57
58
|
* BM = Backend model (optimized for API access)
|
|
59
|
+
* DBM = Database model (logical representation, before compression)
|
|
58
60
|
* TM = Transport model (optimized to be sent over the wire)
|
|
61
|
+
*
|
|
62
|
+
* Note: When auto-compression is enabled, the physical storage format differs from DBM.
|
|
63
|
+
* Compression/decompression is handled transparently at the storage boundary.
|
|
59
64
|
*/
|
|
60
65
|
export class CommonDao<
|
|
61
66
|
BM extends BaseDBEntity,
|
|
@@ -85,6 +90,16 @@ export class CommonDao<
|
|
|
85
90
|
} else {
|
|
86
91
|
delete this.cfg.hooks!.createRandomId
|
|
87
92
|
}
|
|
93
|
+
|
|
94
|
+
// If the auto-compression is enabled,
|
|
95
|
+
// then we need to ensure that the '__compressed' property is part of the index exclusion list.
|
|
96
|
+
if (this.cfg.compress?.keys) {
|
|
97
|
+
const current = this.cfg.excludeFromIndexes
|
|
98
|
+
this.cfg.excludeFromIndexes = current ? [...current] : []
|
|
99
|
+
if (!this.cfg.excludeFromIndexes.includes('__compressed' as any)) {
|
|
100
|
+
this.cfg.excludeFromIndexes.push('__compressed' as any)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
// CREATE
|
|
@@ -139,7 +154,8 @@ export class CommonDao<
|
|
|
139
154
|
private async loadByIds(ids: ID[], opt: CommonDaoReadOptions = {}): Promise<DBM[]> {
|
|
140
155
|
if (!ids.length) return []
|
|
141
156
|
const table = opt.table || this.cfg.table
|
|
142
|
-
|
|
157
|
+
const rows = await (opt.tx || this.cfg.db).getByIds<DBM>(table, ids, opt)
|
|
158
|
+
return await this.storageRowsToDBMs(rows)
|
|
143
159
|
}
|
|
144
160
|
|
|
145
161
|
async getBy(by: keyof DBM, value: any, limit = 0, opt?: CommonDaoReadOptions): Promise<BM[]> {
|
|
@@ -198,8 +214,9 @@ export class CommonDao<
|
|
|
198
214
|
): Promise<RunQueryResult<BM>> {
|
|
199
215
|
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
|
|
200
216
|
q.table = opt.table || q.table
|
|
201
|
-
const { rows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
|
|
217
|
+
const { rows: rawRows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
|
|
202
218
|
const isPartialQuery = !!q._selectedFieldNames
|
|
219
|
+
const rows = isPartialQuery ? rawRows : await this.storageRowsToDBMs(rawRows)
|
|
203
220
|
const bms = isPartialQuery ? (rows as any[]) : await this.dbmsToBM(rows, opt)
|
|
204
221
|
return {
|
|
205
222
|
rows: bms,
|
|
@@ -218,8 +235,9 @@ export class CommonDao<
|
|
|
218
235
|
): Promise<RunQueryResult<DBM>> {
|
|
219
236
|
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
|
|
220
237
|
q.table = opt.table || q.table
|
|
221
|
-
const { rows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
|
|
238
|
+
const { rows: rawRows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
|
|
222
239
|
const isPartialQuery = !!q._selectedFieldNames
|
|
240
|
+
const rows = isPartialQuery ? rawRows : await this.storageRowsToDBMs(rawRows)
|
|
223
241
|
const dbms = isPartialQuery ? rows : await this.anyToDBMs(rows, opt)
|
|
224
242
|
return { rows: dbms, ...queryResult }
|
|
225
243
|
}
|
|
@@ -231,7 +249,13 @@ export class CommonDao<
|
|
|
231
249
|
}
|
|
232
250
|
|
|
233
251
|
streamQueryAsDBM(q: DBQuery<DBM>, opt: CommonDaoStreamOptions<DBM> = {}): Pipeline<DBM> {
|
|
234
|
-
|
|
252
|
+
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
|
|
253
|
+
q.table = opt.table || q.table
|
|
254
|
+
let pipeline = this.cfg.db.streamQuery<DBM>(q, opt)
|
|
255
|
+
|
|
256
|
+
if (this.cfg.compress?.keys.length) {
|
|
257
|
+
pipeline = pipeline.map(async row => await this.storageRowToDBM(row))
|
|
258
|
+
}
|
|
235
259
|
|
|
236
260
|
const isPartialQuery = !!q._selectedFieldNames
|
|
237
261
|
if (isPartialQuery) return pipeline
|
|
@@ -243,7 +267,13 @@ export class CommonDao<
|
|
|
243
267
|
}
|
|
244
268
|
|
|
245
269
|
streamQuery(q: DBQuery<DBM>, opt: CommonDaoStreamOptions<BM> = {}): Pipeline<BM> {
|
|
246
|
-
|
|
270
|
+
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
|
|
271
|
+
q.table = opt.table || q.table
|
|
272
|
+
let pipeline = this.cfg.db.streamQuery<DBM>(q, opt)
|
|
273
|
+
|
|
274
|
+
if (this.cfg.compress?.keys.length) {
|
|
275
|
+
pipeline = pipeline.map(async row => await this.storageRowToDBM(row))
|
|
276
|
+
}
|
|
247
277
|
|
|
248
278
|
const isPartialQuery = !!q._selectedFieldNames
|
|
249
279
|
if (isPartialQuery) return pipeline as any as Pipeline<BM>
|
|
@@ -254,12 +284,6 @@ export class CommonDao<
|
|
|
254
284
|
return pipeline.map(async dbm => await this.dbmToBM(dbm, opt), { errorMode: opt.errorMode })
|
|
255
285
|
}
|
|
256
286
|
|
|
257
|
-
private streamQueryRaw(q: DBQuery<DBM>, opt: CommonDaoStreamOptions<any> = {}): Pipeline<DBM> {
|
|
258
|
-
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
|
|
259
|
-
q.table = opt.table || q.table
|
|
260
|
-
return this.cfg.db.streamQuery<DBM>(q, opt)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
287
|
async queryIds(q: DBQuery<DBM>, opt: CommonDaoReadOptions = {}): Promise<ID[]> {
|
|
264
288
|
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
|
|
265
289
|
q.table = opt.table || q.table
|
|
@@ -447,7 +471,8 @@ export class CommonDao<
|
|
|
447
471
|
const table = opt.table || this.cfg.table
|
|
448
472
|
const saveOptions = this.prepareSaveOptions(opt)
|
|
449
473
|
|
|
450
|
-
|
|
474
|
+
const row = await this.dbmToStorageRow(dbm)
|
|
475
|
+
await (opt.tx || this.cfg.db).saveBatch(table, [row], saveOptions)
|
|
451
476
|
|
|
452
477
|
if (saveOptions.assignGeneratedIds) {
|
|
453
478
|
bm.id = dbm.id
|
|
@@ -459,18 +484,19 @@ export class CommonDao<
|
|
|
459
484
|
async saveAsDBM(dbm: Unsaved<DBM>, opt: CommonDaoSaveOptions<BM, DBM> = {}): Promise<DBM> {
|
|
460
485
|
this.requireWriteAccess()
|
|
461
486
|
this.assignIdCreatedUpdated(dbm, opt) // mutates
|
|
462
|
-
const
|
|
463
|
-
this.cfg.hooks!.beforeSave?.(
|
|
487
|
+
const validDbm = await this.anyToDBM(dbm, opt)
|
|
488
|
+
this.cfg.hooks!.beforeSave?.(validDbm)
|
|
464
489
|
const table = opt.table || this.cfg.table
|
|
465
490
|
const saveOptions = this.prepareSaveOptions(opt)
|
|
466
491
|
|
|
492
|
+
const row = await this.dbmToStorageRow(validDbm)
|
|
467
493
|
await (opt.tx || this.cfg.db).saveBatch(table, [row], saveOptions)
|
|
468
494
|
|
|
469
495
|
if (saveOptions.assignGeneratedIds) {
|
|
470
|
-
dbm.id =
|
|
496
|
+
dbm.id = validDbm.id
|
|
471
497
|
}
|
|
472
498
|
|
|
473
|
-
return
|
|
499
|
+
return validDbm
|
|
474
500
|
}
|
|
475
501
|
|
|
476
502
|
async saveBatch(bms: Unsaved<BM>[], opt: CommonDaoSaveBatchOptions<DBM> = {}): Promise<BM[]> {
|
|
@@ -484,7 +510,8 @@ export class CommonDao<
|
|
|
484
510
|
const table = opt.table || this.cfg.table
|
|
485
511
|
const saveOptions = this.prepareSaveOptions(opt)
|
|
486
512
|
|
|
487
|
-
|
|
513
|
+
const rows = await this.dbmsToStorageRows(dbms)
|
|
514
|
+
await (opt.tx || this.cfg.db).saveBatch(table, rows, saveOptions)
|
|
488
515
|
|
|
489
516
|
if (saveOptions.assignGeneratedIds) {
|
|
490
517
|
dbms.forEach((dbm, i) => (bms[i]!.id = dbm.id))
|
|
@@ -500,33 +527,39 @@ export class CommonDao<
|
|
|
500
527
|
if (!dbms.length) return []
|
|
501
528
|
this.requireWriteAccess()
|
|
502
529
|
dbms.forEach(dbm => this.assignIdCreatedUpdated(dbm, opt))
|
|
503
|
-
const
|
|
530
|
+
const validDbms = await this.anyToDBMs(dbms as DBM[], opt)
|
|
504
531
|
if (this.cfg.hooks!.beforeSave) {
|
|
505
|
-
|
|
532
|
+
validDbms.forEach(dbm => this.cfg.hooks!.beforeSave!(dbm))
|
|
506
533
|
}
|
|
507
534
|
const table = opt.table || this.cfg.table
|
|
508
535
|
const saveOptions = this.prepareSaveOptions(opt)
|
|
509
536
|
|
|
537
|
+
const rows = await this.dbmsToStorageRows(validDbms)
|
|
510
538
|
await (opt.tx || this.cfg.db).saveBatch(table, rows, saveOptions)
|
|
511
539
|
|
|
512
540
|
if (saveOptions.assignGeneratedIds) {
|
|
513
|
-
|
|
541
|
+
validDbms.forEach((dbm, i) => (dbms[i]!.id = dbm.id))
|
|
514
542
|
}
|
|
515
543
|
|
|
516
|
-
return
|
|
544
|
+
return validDbms
|
|
517
545
|
}
|
|
518
546
|
|
|
519
|
-
private prepareSaveOptions(
|
|
547
|
+
private prepareSaveOptions(
|
|
548
|
+
opt: CommonDaoSaveOptions<BM, DBM>,
|
|
549
|
+
): CommonDBSaveOptions<ObjectWithId> {
|
|
520
550
|
let {
|
|
521
551
|
saveMethod,
|
|
522
552
|
assignGeneratedIds = this.cfg.assignGeneratedIds,
|
|
523
553
|
excludeFromIndexes = this.cfg.excludeFromIndexes,
|
|
524
554
|
} = opt
|
|
525
555
|
|
|
556
|
+
// If the user passed in custom `excludeFromIndexes` with the save() call,
|
|
557
|
+
// and the auto-compression is enabled,
|
|
558
|
+
// then we need to ensure that the '__compressed' property is part of the list.
|
|
526
559
|
if (this.cfg.compress?.keys) {
|
|
527
560
|
excludeFromIndexes ??= []
|
|
528
|
-
if (!excludeFromIndexes.includes('
|
|
529
|
-
excludeFromIndexes.push('
|
|
561
|
+
if (!excludeFromIndexes.includes('__compressed' as any)) {
|
|
562
|
+
excludeFromIndexes.push('__compressed' as any)
|
|
530
563
|
}
|
|
531
564
|
}
|
|
532
565
|
|
|
@@ -536,7 +569,7 @@ export class CommonDao<
|
|
|
536
569
|
|
|
537
570
|
return {
|
|
538
571
|
...opt,
|
|
539
|
-
excludeFromIndexes,
|
|
572
|
+
excludeFromIndexes: excludeFromIndexes as (keyof ObjectWithId)[],
|
|
540
573
|
saveMethod,
|
|
541
574
|
assignGeneratedIds,
|
|
542
575
|
}
|
|
@@ -558,11 +591,7 @@ export class CommonDao<
|
|
|
558
591
|
opt.skipValidation ??= true
|
|
559
592
|
opt.errorMode ||= ErrorMode.SUPPRESS
|
|
560
593
|
|
|
561
|
-
|
|
562
|
-
opt = { ...opt, saveMethod: 'insert' }
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const excludeFromIndexes = opt.excludeFromIndexes || this.cfg.excludeFromIndexes
|
|
594
|
+
const saveOptions = this.prepareSaveOptions(opt)
|
|
566
595
|
const { beforeSave } = this.cfg.hooks!
|
|
567
596
|
|
|
568
597
|
const { chunkSize = 500, chunkConcurrency = 32, errorMode } = opt
|
|
@@ -573,17 +602,14 @@ export class CommonDao<
|
|
|
573
602
|
this.assignIdCreatedUpdated(bm, opt)
|
|
574
603
|
const dbm = await this.bmToDBM(bm, opt)
|
|
575
604
|
beforeSave?.(dbm)
|
|
576
|
-
return dbm
|
|
605
|
+
return await this.dbmToStorageRow(dbm)
|
|
577
606
|
},
|
|
578
607
|
{ errorMode },
|
|
579
608
|
)
|
|
580
609
|
.chunk(chunkSize)
|
|
581
610
|
.map(
|
|
582
611
|
async batch => {
|
|
583
|
-
await this.cfg.db.saveBatch(table, batch,
|
|
584
|
-
...opt,
|
|
585
|
-
excludeFromIndexes,
|
|
586
|
-
})
|
|
612
|
+
await this.cfg.db.saveBatch(table, batch, saveOptions)
|
|
587
613
|
return batch
|
|
588
614
|
},
|
|
589
615
|
{
|
|
@@ -722,9 +748,6 @@ export class CommonDao<
|
|
|
722
748
|
// const dbm = this.anyToDBM(_dbm, opt)
|
|
723
749
|
const dbm: DBM = { ..._dbm, ...this.cfg.hooks!.parseNaturalId!(_dbm.id as ID) }
|
|
724
750
|
|
|
725
|
-
// Decompress
|
|
726
|
-
await this.decompress(dbm)
|
|
727
|
-
|
|
728
751
|
// DBM > BM
|
|
729
752
|
const bm = ((await this.cfg.hooks!.beforeDBMToBM?.(dbm)) || dbm) as Partial<BM>
|
|
730
753
|
|
|
@@ -751,9 +774,6 @@ export class CommonDao<
|
|
|
751
774
|
// BM > DBM
|
|
752
775
|
const dbm = ((await this.cfg.hooks!.beforeBMToDBM?.(bm)) || bm) as DBM
|
|
753
776
|
|
|
754
|
-
// Compress
|
|
755
|
-
if (this.cfg.compress) await this.compress(dbm)
|
|
756
|
-
|
|
757
777
|
return dbm
|
|
758
778
|
}
|
|
759
779
|
|
|
@@ -762,37 +782,85 @@ export class CommonDao<
|
|
|
762
782
|
return await pMap(bms, async bm => await this.bmToDBM(bm, opt))
|
|
763
783
|
}
|
|
764
784
|
|
|
785
|
+
// STORAGE LAYER (compression/decompression at DB boundary)
|
|
786
|
+
// These methods convert between DBM (logical model) and storage format (physical, possibly compressed).
|
|
787
|
+
// Public methods allow external code to bypass the DAO layer for direct DB access
|
|
788
|
+
// (e.g., cross-environment data copy).
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Converts a DBM to storage format, applying compression if configured.
|
|
792
|
+
*
|
|
793
|
+
* Use this when you need to write directly to the database, bypassing the DAO save methods.
|
|
794
|
+
* The returned value is opaque and should only be passed to db.saveBatch() or similar.
|
|
795
|
+
*
|
|
796
|
+
* @example
|
|
797
|
+
* const storageRow = await dao.dbmToStorageRow(dbm)
|
|
798
|
+
* await db.saveBatch(table, [storageRow])
|
|
799
|
+
*/
|
|
800
|
+
async dbmToStorageRow(dbm: DBM): Promise<ObjectWithId> {
|
|
801
|
+
if (!this.cfg.compress?.keys.length) return dbm
|
|
802
|
+
const row = { ...dbm }
|
|
803
|
+
await this.compress(row)
|
|
804
|
+
return row
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Converts multiple DBMs to storage rows.
|
|
809
|
+
*/
|
|
810
|
+
async dbmsToStorageRows(dbms: DBM[]): Promise<ObjectWithId[]> {
|
|
811
|
+
if (!this.cfg.compress?.keys.length) return dbms
|
|
812
|
+
return await pMap(dbms, async dbm => await this.dbmToStorageRow(dbm))
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Converts a storage row back to a DBM, applying decompression if needed.
|
|
817
|
+
*
|
|
818
|
+
* Use this when you need to read directly from the database, bypassing the DAO load methods.
|
|
819
|
+
*
|
|
820
|
+
* @example
|
|
821
|
+
* const rows = await db.getByIds(table, ids)
|
|
822
|
+
* const dbms = await Promise.all(rows.map(row => dao.storageRowToDBM(row)))
|
|
823
|
+
*/
|
|
824
|
+
async storageRowToDBM(row: ObjectWithId): Promise<DBM> {
|
|
825
|
+
if (!this.cfg.compress?.keys.length) return row as DBM
|
|
826
|
+
const dbm = { ...(row as DBM) }
|
|
827
|
+
await this.decompress(dbm)
|
|
828
|
+
return dbm
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Converts multiple storage rows to DBMs.
|
|
833
|
+
*/
|
|
834
|
+
async storageRowsToDBMs(rows: ObjectWithId[]): Promise<DBM[]> {
|
|
835
|
+
if (!this.cfg.compress?.keys.length) return rows as DBM[]
|
|
836
|
+
return await pMap(rows, async row => await this.storageRowToDBM(row))
|
|
837
|
+
}
|
|
838
|
+
|
|
765
839
|
/**
|
|
766
840
|
* Mutates `dbm`.
|
|
767
841
|
*/
|
|
768
|
-
async compress(dbm: DBM): Promise<void> {
|
|
842
|
+
private async compress(dbm: DBM): Promise<void> {
|
|
769
843
|
if (!this.cfg.compress?.keys.length) return // No compression requested
|
|
770
844
|
|
|
771
845
|
const { keys } = this.cfg.compress
|
|
772
846
|
const properties = _pick(dbm, keys)
|
|
773
|
-
_assert(
|
|
774
|
-
!('data' in dbm) || 'data' in properties,
|
|
775
|
-
`Data (${dbm.id}) already has a "data" property. When using compression, this property must be included in the compression keys list.`,
|
|
776
|
-
)
|
|
777
847
|
const bufferString = JSON.stringify(properties)
|
|
778
|
-
const
|
|
848
|
+
const __compressed = await zstdCompress(bufferString)
|
|
779
849
|
_omitWithUndefined(dbm as any, _objectKeys(properties), { mutate: true })
|
|
780
|
-
Object.assign(dbm, {
|
|
850
|
+
Object.assign(dbm, { __compressed })
|
|
781
851
|
}
|
|
782
852
|
|
|
783
853
|
/**
|
|
784
854
|
* Mutates `dbm`.
|
|
785
855
|
*/
|
|
786
|
-
async decompress(dbm: DBM): Promise<void> {
|
|
856
|
+
private async decompress(dbm: DBM): Promise<void> {
|
|
787
857
|
_typeCast<Compressed<DBM>>(dbm)
|
|
788
|
-
if (!
|
|
789
|
-
if (!Buffer.isBuffer(dbm.data)) return // No compressed data
|
|
858
|
+
if (!Buffer.isBuffer(dbm.__compressed)) return // No compressed data
|
|
790
859
|
|
|
791
|
-
// try-catch to avoid a `data` with Buffer which is not compressed, but legit data
|
|
792
860
|
try {
|
|
793
|
-
const bufferString = await decompressZstdOrInflateToString(dbm.
|
|
861
|
+
const bufferString = await decompressZstdOrInflateToString(dbm.__compressed)
|
|
794
862
|
const properties = JSON.parse(bufferString)
|
|
795
|
-
dbm.
|
|
863
|
+
dbm.__compressed = undefined
|
|
796
864
|
Object.assign(dbm, properties)
|
|
797
865
|
} catch {}
|
|
798
866
|
}
|
|
@@ -807,9 +875,6 @@ export class CommonDao<
|
|
|
807
875
|
|
|
808
876
|
dbm = { ...dbm, ...this.cfg.hooks!.parseNaturalId!(dbm.id as ID) }
|
|
809
877
|
|
|
810
|
-
// Decompress
|
|
811
|
-
await this.decompress(dbm)
|
|
812
|
-
|
|
813
878
|
// Validate/convert DBM
|
|
814
879
|
// return this.validateAndConvert(dbm, this.cfg.dbmSchema, DBModelType.DBM, opt)
|
|
815
880
|
return dbm
|
|
@@ -906,6 +971,75 @@ export class CommonDao<
|
|
|
906
971
|
}
|
|
907
972
|
}
|
|
908
973
|
|
|
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>(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
|
+
|
|
909
1043
|
/**
|
|
910
1044
|
* Load rows (by their ids) from Multiple tables at once.
|
|
911
1045
|
* An optimized way to load data, minimizing DB round-trips.
|
|
@@ -991,13 +1125,17 @@ export class CommonDao<
|
|
|
991
1125
|
const { table } = dao.cfg
|
|
992
1126
|
if ('id' in input) {
|
|
993
1127
|
// Singular
|
|
994
|
-
const
|
|
1128
|
+
const row = dbmByTableById[table]![input.id]
|
|
1129
|
+
// Decompress before converting to BM
|
|
1130
|
+
const dbm = row ? await dao.storageRowToDBM(row) : undefined
|
|
995
1131
|
bmsByProp[prop] = (await dao.dbmToBM(dbm, opt)) || null
|
|
996
1132
|
} else {
|
|
997
1133
|
// Plural
|
|
998
1134
|
// We apply filtering, to be able to support multiple input props fetching from the same table.
|
|
999
1135
|
// Without filtering - every prop will get ALL rows from that table.
|
|
1000
|
-
const
|
|
1136
|
+
const rows = input.ids.map(id => dbmByTableById[table]![id]).filter(_isTruthy)
|
|
1137
|
+
// Decompress before converting to BM
|
|
1138
|
+
const dbms = await dao.storageRowsToDBMs(rows)
|
|
1001
1139
|
bmsByProp[prop] = await dao.dbmsToBM(dbms, opt)
|
|
1002
1140
|
}
|
|
1003
1141
|
})
|
|
@@ -1062,7 +1200,8 @@ export class CommonDao<
|
|
|
1062
1200
|
dao.assignIdCreatedUpdated(row, opt)
|
|
1063
1201
|
const dbm = await dao.bmToDBM(row, opt)
|
|
1064
1202
|
dao.cfg.hooks!.beforeSave?.(dbm)
|
|
1065
|
-
|
|
1203
|
+
const storageRow = await dao.dbmToStorageRow(dbm)
|
|
1204
|
+
dbmsByTable[table].push(storageRow)
|
|
1066
1205
|
} else {
|
|
1067
1206
|
// Plural
|
|
1068
1207
|
input.rows.forEach(bm => dao.assignIdCreatedUpdated(bm, opt))
|
|
@@ -1070,7 +1209,8 @@ export class CommonDao<
|
|
|
1070
1209
|
if (dao.cfg.hooks!.beforeSave) {
|
|
1071
1210
|
dbms.forEach(dbm => dao.cfg.hooks!.beforeSave!(dbm))
|
|
1072
1211
|
}
|
|
1073
|
-
|
|
1212
|
+
const storageRows = await dao.dbmsToStorageRows(dbms)
|
|
1213
|
+
dbmsByTable[table].push(...storageRows)
|
|
1074
1214
|
}
|
|
1075
1215
|
})
|
|
1076
1216
|
|
|
@@ -1205,4 +1345,4 @@ export type AnyDao = CommonDao<any>
|
|
|
1205
1345
|
* Used internally during compression/decompression so that DBM instances can
|
|
1206
1346
|
* carry their compressed payload alongside the original type shape.
|
|
1207
1347
|
*/
|
|
1208
|
-
type Compressed<DBM> = DBM & {
|
|
1348
|
+
type Compressed<DBM> = DBM & { __compressed?: Buffer }
|
|
@@ -2,13 +2,8 @@ import { _isEmptyObject } from '@naturalcycles/js-lib'
|
|
|
2
2
|
import { _assert } from '@naturalcycles/js-lib/error/assert.js'
|
|
3
3
|
import type { CommonLogger } from '@naturalcycles/js-lib/log'
|
|
4
4
|
import { _deepCopy, _sortObjectDeep } from '@naturalcycles/js-lib/object'
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
_stringMapValues,
|
|
8
|
-
type AnyObjectWithId,
|
|
9
|
-
type ObjectWithId,
|
|
10
|
-
type StringMap,
|
|
11
|
-
} from '@naturalcycles/js-lib/types'
|
|
5
|
+
import { _stringMapEntries, _stringMapValues } from '@naturalcycles/js-lib/types'
|
|
6
|
+
import type { AnyObjectWithId, ObjectWithId, StringMap } from '@naturalcycles/js-lib/types'
|
|
12
7
|
import type { JsonSchema } from '@naturalcycles/nodejs-lib/ajv'
|
|
13
8
|
import { generateJsonSchemaFromData } from '@naturalcycles/nodejs-lib/ajv'
|
|
14
9
|
import { Pipeline } from '@naturalcycles/nodejs-lib/stream'
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AppError } from '@naturalcycles/js-lib/error/error.util.js'
|
|
2
2
|
import type { CommonLogger } from '@naturalcycles/js-lib/log'
|
|
3
3
|
import { pMap } from '@naturalcycles/js-lib/promise/pMap.js'
|
|
4
|
-
import {
|
|
4
|
+
import { SKIP } from '@naturalcycles/js-lib/types'
|
|
5
|
+
import type { Integer, KeyValueTuple } from '@naturalcycles/js-lib/types'
|
|
5
6
|
import type { Pipeline } from '@naturalcycles/nodejs-lib/stream'
|
|
6
7
|
import {
|
|
7
8
|
decompressZstdOrInflateToString,
|
|
@@ -8,11 +8,10 @@ import { _passthroughMapper } from '@naturalcycles/js-lib/types'
|
|
|
8
8
|
import type { JsonSchema } from '@naturalcycles/nodejs-lib/ajv'
|
|
9
9
|
import { boldWhite, dimWhite, grey, yellow } from '@naturalcycles/nodejs-lib/colors'
|
|
10
10
|
import { fs2 } from '@naturalcycles/nodejs-lib/fs2'
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type TransformMapOptions,
|
|
11
|
+
import { NDJsonStats, Pipeline } from '@naturalcycles/nodejs-lib/stream'
|
|
12
|
+
import type {
|
|
13
|
+
TransformLogProgressOptions,
|
|
14
|
+
TransformMapOptions,
|
|
16
15
|
} from '@naturalcycles/nodejs-lib/stream'
|
|
17
16
|
import type { CommonDB } from '../commondb/common.db.js'
|
|
18
17
|
import type { CommonDBSaveOptions } from '../db.model.js'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { _range } from '@naturalcycles/js-lib/array/range.js'
|
|
2
2
|
import type { BaseDBEntity, UnixTimestamp } from '@naturalcycles/js-lib/types'
|
|
3
|
-
import { j
|
|
3
|
+
import { j } from '@naturalcycles/nodejs-lib/ajv'
|
|
4
|
+
import type { JsonSchemaObjectBuilder } from '@naturalcycles/nodejs-lib/ajv'
|
|
4
5
|
|
|
5
6
|
const MOCK_TS_2018_06_21 = 1529539200 as UnixTimestamp
|
|
6
7
|
|
package/src/validation/index.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import type { ObjectWithId } from '@naturalcycles/js-lib/types'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
JsonSchemaAnyBuilder,
|
|
5
|
-
type JsonSchemaObjectBuilder,
|
|
6
|
-
} from '@naturalcycles/nodejs-lib/ajv'
|
|
2
|
+
import { j, JsonSchemaAnyBuilder } from '@naturalcycles/nodejs-lib/ajv'
|
|
3
|
+
import type { JsonSchemaObjectBuilder } from '@naturalcycles/nodejs-lib/ajv'
|
|
7
4
|
import type { CommonDBOptions, CommonDBSaveOptions, DBTransaction } from '../db.model.js'
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
dbQueryFilterOperatorValues,
|
|
11
|
-
type DBQueryOrder,
|
|
12
|
-
} from '../query/dbQuery.js'
|
|
5
|
+
import { dbQueryFilterOperatorValues } from '../query/dbQuery.js'
|
|
6
|
+
import type { DBQueryFilter, DBQueryOrder } from '../query/dbQuery.js'
|
|
13
7
|
|
|
14
8
|
// oxlint-disable typescript/explicit-function-return-type
|
|
15
9
|
|