@naturalcycles/db-lib 10.41.1 → 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 +193 -51
- 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 +214 -66
- 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,26 +395,36 @@ 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.
|
|
416
|
+
if (this.cfg.compress?.keys) {
|
|
417
|
+
excludeFromIndexes ??= [];
|
|
418
|
+
if (!excludeFromIndexes.includes('__compressed')) {
|
|
419
|
+
excludeFromIndexes.push('__compressed');
|
|
420
|
+
}
|
|
421
|
+
}
|
|
389
422
|
if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
|
|
390
423
|
saveMethod = 'insert';
|
|
391
424
|
}
|
|
392
425
|
return {
|
|
393
426
|
...opt,
|
|
394
|
-
excludeFromIndexes,
|
|
427
|
+
excludeFromIndexes: excludeFromIndexes,
|
|
395
428
|
saveMethod,
|
|
396
429
|
assignGeneratedIds,
|
|
397
430
|
};
|
|
@@ -410,10 +443,7 @@ export class CommonDao {
|
|
|
410
443
|
const table = opt.table || this.cfg.table;
|
|
411
444
|
opt.skipValidation ??= true;
|
|
412
445
|
opt.errorMode ||= ErrorMode.SUPPRESS;
|
|
413
|
-
|
|
414
|
-
opt = { ...opt, saveMethod: 'insert' };
|
|
415
|
-
}
|
|
416
|
-
const excludeFromIndexes = opt.excludeFromIndexes || this.cfg.excludeFromIndexes;
|
|
446
|
+
const saveOptions = this.prepareSaveOptions(opt);
|
|
417
447
|
const { beforeSave } = this.cfg.hooks;
|
|
418
448
|
const { chunkSize = 500, chunkConcurrency = 32, errorMode } = opt;
|
|
419
449
|
await p
|
|
@@ -421,14 +451,11 @@ export class CommonDao {
|
|
|
421
451
|
this.assignIdCreatedUpdated(bm, opt);
|
|
422
452
|
const dbm = await this.bmToDBM(bm, opt);
|
|
423
453
|
beforeSave?.(dbm);
|
|
424
|
-
return dbm;
|
|
454
|
+
return await this.dbmToStorageRow(dbm);
|
|
425
455
|
}, { errorMode })
|
|
426
456
|
.chunk(chunkSize)
|
|
427
457
|
.map(async (batch) => {
|
|
428
|
-
await this.cfg.db.saveBatch(table, batch,
|
|
429
|
-
...opt,
|
|
430
|
-
excludeFromIndexes,
|
|
431
|
-
});
|
|
458
|
+
await this.cfg.db.saveBatch(table, batch, saveOptions);
|
|
432
459
|
return batch;
|
|
433
460
|
}, {
|
|
434
461
|
concurrency: chunkConcurrency,
|
|
@@ -539,8 +566,6 @@ export class CommonDao {
|
|
|
539
566
|
// optimization: no need to run full joi DBM validation, cause BM validation will be run
|
|
540
567
|
// const dbm = this.anyToDBM(_dbm, opt)
|
|
541
568
|
const dbm = { ..._dbm, ...this.cfg.hooks.parseNaturalId(_dbm.id) };
|
|
542
|
-
// Decompress
|
|
543
|
-
await this.decompress(dbm);
|
|
544
569
|
// DBM > BM
|
|
545
570
|
const bm = ((await this.cfg.hooks.beforeDBMToBM?.(dbm)) || dbm);
|
|
546
571
|
// Validate/convert BM
|
|
@@ -556,15 +581,65 @@ export class CommonDao {
|
|
|
556
581
|
bm = this.validateAndConvert(bm, 'save', opt);
|
|
557
582
|
// BM > DBM
|
|
558
583
|
const dbm = ((await this.cfg.hooks.beforeBMToDBM?.(bm)) || bm);
|
|
559
|
-
// Compress
|
|
560
|
-
if (this.cfg.compress)
|
|
561
|
-
await this.compress(dbm);
|
|
562
584
|
return dbm;
|
|
563
585
|
}
|
|
564
586
|
async bmsToDBM(bms, opt = {}) {
|
|
565
587
|
// try/catch?
|
|
566
588
|
return await pMap(bms, async (bm) => await this.bmToDBM(bm, opt));
|
|
567
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
|
+
}
|
|
568
643
|
/**
|
|
569
644
|
* Mutates `dbm`.
|
|
570
645
|
*/
|
|
@@ -573,26 +648,22 @@ export class CommonDao {
|
|
|
573
648
|
return; // No compression requested
|
|
574
649
|
const { keys } = this.cfg.compress;
|
|
575
650
|
const properties = _pick(dbm, keys);
|
|
576
|
-
_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.`);
|
|
577
651
|
const bufferString = JSON.stringify(properties);
|
|
578
|
-
const
|
|
652
|
+
const __compressed = await zstdCompress(bufferString);
|
|
579
653
|
_omitWithUndefined(dbm, _objectKeys(properties), { mutate: true });
|
|
580
|
-
Object.assign(dbm, {
|
|
654
|
+
Object.assign(dbm, { __compressed });
|
|
581
655
|
}
|
|
582
656
|
/**
|
|
583
657
|
* Mutates `dbm`.
|
|
584
658
|
*/
|
|
585
659
|
async decompress(dbm) {
|
|
586
660
|
_typeCast(dbm);
|
|
587
|
-
if (!
|
|
588
|
-
return; // Auto-compression not turned on
|
|
589
|
-
if (!Buffer.isBuffer(dbm.data))
|
|
661
|
+
if (!Buffer.isBuffer(dbm.__compressed))
|
|
590
662
|
return; // No compressed data
|
|
591
|
-
// try-catch to avoid a `data` with Buffer which is not compressed, but legit data
|
|
592
663
|
try {
|
|
593
|
-
const bufferString = await decompressZstdOrInflateToString(dbm.
|
|
664
|
+
const bufferString = await decompressZstdOrInflateToString(dbm.__compressed);
|
|
594
665
|
const properties = JSON.parse(bufferString);
|
|
595
|
-
dbm.
|
|
666
|
+
dbm.__compressed = undefined;
|
|
596
667
|
Object.assign(dbm, properties);
|
|
597
668
|
}
|
|
598
669
|
catch { }
|
|
@@ -603,8 +674,6 @@ export class CommonDao {
|
|
|
603
674
|
// this shouldn't be happening on load! but should on save!
|
|
604
675
|
// this.assignIdCreatedUpdated(dbm, opt)
|
|
605
676
|
dbm = { ...dbm, ...this.cfg.hooks.parseNaturalId(dbm.id) };
|
|
606
|
-
// Decompress
|
|
607
|
-
await this.decompress(dbm);
|
|
608
677
|
// Validate/convert DBM
|
|
609
678
|
// return this.validateAndConvert(dbm, this.cfg.dbmSchema, DBModelType.DBM, opt)
|
|
610
679
|
return dbm;
|
|
@@ -682,6 +751,73 @@ export class CommonDao {
|
|
|
682
751
|
opt: opt,
|
|
683
752
|
};
|
|
684
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
|
+
}
|
|
685
821
|
/**
|
|
686
822
|
* Load rows (by their ids) from Multiple tables at once.
|
|
687
823
|
* An optimized way to load data, minimizing DB round-trips.
|
|
@@ -742,14 +878,18 @@ export class CommonDao {
|
|
|
742
878
|
const { table } = dao.cfg;
|
|
743
879
|
if ('id' in input) {
|
|
744
880
|
// Singular
|
|
745
|
-
const
|
|
881
|
+
const row = dbmByTableById[table][input.id];
|
|
882
|
+
// Decompress before converting to BM
|
|
883
|
+
const dbm = row ? await dao.storageRowToDBM(row) : undefined;
|
|
746
884
|
bmsByProp[prop] = (await dao.dbmToBM(dbm, opt)) || null;
|
|
747
885
|
}
|
|
748
886
|
else {
|
|
749
887
|
// Plural
|
|
750
888
|
// We apply filtering, to be able to support multiple input props fetching from the same table.
|
|
751
889
|
// Without filtering - every prop will get ALL rows from that table.
|
|
752
|
-
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);
|
|
753
893
|
bmsByProp[prop] = await dao.dbmsToBM(dbms, opt);
|
|
754
894
|
}
|
|
755
895
|
});
|
|
@@ -803,7 +943,8 @@ export class CommonDao {
|
|
|
803
943
|
dao.assignIdCreatedUpdated(row, opt);
|
|
804
944
|
const dbm = await dao.bmToDBM(row, opt);
|
|
805
945
|
dao.cfg.hooks.beforeSave?.(dbm);
|
|
806
|
-
|
|
946
|
+
const storageRow = await dao.dbmToStorageRow(dbm);
|
|
947
|
+
dbmsByTable[table].push(storageRow);
|
|
807
948
|
}
|
|
808
949
|
else {
|
|
809
950
|
// Plural
|
|
@@ -812,7 +953,8 @@ export class CommonDao {
|
|
|
812
953
|
if (dao.cfg.hooks.beforeSave) {
|
|
813
954
|
dbms.forEach(dbm => dao.cfg.hooks.beforeSave(dbm));
|
|
814
955
|
}
|
|
815
|
-
|
|
956
|
+
const storageRows = await dao.dbmsToStorageRows(dbms);
|
|
957
|
+
dbmsByTable[table].push(...storageRows);
|
|
816
958
|
}
|
|
817
959
|
});
|
|
818
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.
|
|
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,35 +527,49 @@ 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
|
|
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.
|
|
559
|
+
if (this.cfg.compress?.keys) {
|
|
560
|
+
excludeFromIndexes ??= []
|
|
561
|
+
if (!excludeFromIndexes.includes('__compressed' as any)) {
|
|
562
|
+
excludeFromIndexes.push('__compressed' as any)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
525
566
|
if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
|
|
526
567
|
saveMethod = 'insert'
|
|
527
568
|
}
|
|
528
569
|
|
|
529
570
|
return {
|
|
530
571
|
...opt,
|
|
531
|
-
excludeFromIndexes,
|
|
572
|
+
excludeFromIndexes: excludeFromIndexes as (keyof ObjectWithId)[],
|
|
532
573
|
saveMethod,
|
|
533
574
|
assignGeneratedIds,
|
|
534
575
|
}
|
|
@@ -550,11 +591,7 @@ export class CommonDao<
|
|
|
550
591
|
opt.skipValidation ??= true
|
|
551
592
|
opt.errorMode ||= ErrorMode.SUPPRESS
|
|
552
593
|
|
|
553
|
-
|
|
554
|
-
opt = { ...opt, saveMethod: 'insert' }
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
const excludeFromIndexes = opt.excludeFromIndexes || this.cfg.excludeFromIndexes
|
|
594
|
+
const saveOptions = this.prepareSaveOptions(opt)
|
|
558
595
|
const { beforeSave } = this.cfg.hooks!
|
|
559
596
|
|
|
560
597
|
const { chunkSize = 500, chunkConcurrency = 32, errorMode } = opt
|
|
@@ -565,17 +602,14 @@ export class CommonDao<
|
|
|
565
602
|
this.assignIdCreatedUpdated(bm, opt)
|
|
566
603
|
const dbm = await this.bmToDBM(bm, opt)
|
|
567
604
|
beforeSave?.(dbm)
|
|
568
|
-
return dbm
|
|
605
|
+
return await this.dbmToStorageRow(dbm)
|
|
569
606
|
},
|
|
570
607
|
{ errorMode },
|
|
571
608
|
)
|
|
572
609
|
.chunk(chunkSize)
|
|
573
610
|
.map(
|
|
574
611
|
async batch => {
|
|
575
|
-
await this.cfg.db.saveBatch(table, batch,
|
|
576
|
-
...opt,
|
|
577
|
-
excludeFromIndexes,
|
|
578
|
-
})
|
|
612
|
+
await this.cfg.db.saveBatch(table, batch, saveOptions)
|
|
579
613
|
return batch
|
|
580
614
|
},
|
|
581
615
|
{
|
|
@@ -714,9 +748,6 @@ export class CommonDao<
|
|
|
714
748
|
// const dbm = this.anyToDBM(_dbm, opt)
|
|
715
749
|
const dbm: DBM = { ..._dbm, ...this.cfg.hooks!.parseNaturalId!(_dbm.id as ID) }
|
|
716
750
|
|
|
717
|
-
// Decompress
|
|
718
|
-
await this.decompress(dbm)
|
|
719
|
-
|
|
720
751
|
// DBM > BM
|
|
721
752
|
const bm = ((await this.cfg.hooks!.beforeDBMToBM?.(dbm)) || dbm) as Partial<BM>
|
|
722
753
|
|
|
@@ -743,9 +774,6 @@ export class CommonDao<
|
|
|
743
774
|
// BM > DBM
|
|
744
775
|
const dbm = ((await this.cfg.hooks!.beforeBMToDBM?.(bm)) || bm) as DBM
|
|
745
776
|
|
|
746
|
-
// Compress
|
|
747
|
-
if (this.cfg.compress) await this.compress(dbm)
|
|
748
|
-
|
|
749
777
|
return dbm
|
|
750
778
|
}
|
|
751
779
|
|
|
@@ -754,37 +782,85 @@ export class CommonDao<
|
|
|
754
782
|
return await pMap(bms, async bm => await this.bmToDBM(bm, opt))
|
|
755
783
|
}
|
|
756
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
|
+
|
|
757
839
|
/**
|
|
758
840
|
* Mutates `dbm`.
|
|
759
841
|
*/
|
|
760
|
-
async compress(dbm: DBM): Promise<void> {
|
|
842
|
+
private async compress(dbm: DBM): Promise<void> {
|
|
761
843
|
if (!this.cfg.compress?.keys.length) return // No compression requested
|
|
762
844
|
|
|
763
845
|
const { keys } = this.cfg.compress
|
|
764
846
|
const properties = _pick(dbm, keys)
|
|
765
|
-
_assert(
|
|
766
|
-
!('data' in dbm) || 'data' in properties,
|
|
767
|
-
`Data (${dbm.id}) already has a "data" property. When using compression, this property must be included in the compression keys list.`,
|
|
768
|
-
)
|
|
769
847
|
const bufferString = JSON.stringify(properties)
|
|
770
|
-
const
|
|
848
|
+
const __compressed = await zstdCompress(bufferString)
|
|
771
849
|
_omitWithUndefined(dbm as any, _objectKeys(properties), { mutate: true })
|
|
772
|
-
Object.assign(dbm, {
|
|
850
|
+
Object.assign(dbm, { __compressed })
|
|
773
851
|
}
|
|
774
852
|
|
|
775
853
|
/**
|
|
776
854
|
* Mutates `dbm`.
|
|
777
855
|
*/
|
|
778
|
-
async decompress(dbm: DBM): Promise<void> {
|
|
856
|
+
private async decompress(dbm: DBM): Promise<void> {
|
|
779
857
|
_typeCast<Compressed<DBM>>(dbm)
|
|
780
|
-
if (!
|
|
781
|
-
if (!Buffer.isBuffer(dbm.data)) return // No compressed data
|
|
858
|
+
if (!Buffer.isBuffer(dbm.__compressed)) return // No compressed data
|
|
782
859
|
|
|
783
|
-
// try-catch to avoid a `data` with Buffer which is not compressed, but legit data
|
|
784
860
|
try {
|
|
785
|
-
const bufferString = await decompressZstdOrInflateToString(dbm.
|
|
861
|
+
const bufferString = await decompressZstdOrInflateToString(dbm.__compressed)
|
|
786
862
|
const properties = JSON.parse(bufferString)
|
|
787
|
-
dbm.
|
|
863
|
+
dbm.__compressed = undefined
|
|
788
864
|
Object.assign(dbm, properties)
|
|
789
865
|
} catch {}
|
|
790
866
|
}
|
|
@@ -799,9 +875,6 @@ export class CommonDao<
|
|
|
799
875
|
|
|
800
876
|
dbm = { ...dbm, ...this.cfg.hooks!.parseNaturalId!(dbm.id as ID) }
|
|
801
877
|
|
|
802
|
-
// Decompress
|
|
803
|
-
await this.decompress(dbm)
|
|
804
|
-
|
|
805
878
|
// Validate/convert DBM
|
|
806
879
|
// return this.validateAndConvert(dbm, this.cfg.dbmSchema, DBModelType.DBM, opt)
|
|
807
880
|
return dbm
|
|
@@ -898,6 +971,75 @@ export class CommonDao<
|
|
|
898
971
|
}
|
|
899
972
|
}
|
|
900
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
|
+
|
|
901
1043
|
/**
|
|
902
1044
|
* Load rows (by their ids) from Multiple tables at once.
|
|
903
1045
|
* An optimized way to load data, minimizing DB round-trips.
|
|
@@ -983,13 +1125,17 @@ export class CommonDao<
|
|
|
983
1125
|
const { table } = dao.cfg
|
|
984
1126
|
if ('id' in input) {
|
|
985
1127
|
// Singular
|
|
986
|
-
const
|
|
1128
|
+
const row = dbmByTableById[table]![input.id]
|
|
1129
|
+
// Decompress before converting to BM
|
|
1130
|
+
const dbm = row ? await dao.storageRowToDBM(row) : undefined
|
|
987
1131
|
bmsByProp[prop] = (await dao.dbmToBM(dbm, opt)) || null
|
|
988
1132
|
} else {
|
|
989
1133
|
// Plural
|
|
990
1134
|
// We apply filtering, to be able to support multiple input props fetching from the same table.
|
|
991
1135
|
// Without filtering - every prop will get ALL rows from that table.
|
|
992
|
-
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)
|
|
993
1139
|
bmsByProp[prop] = await dao.dbmsToBM(dbms, opt)
|
|
994
1140
|
}
|
|
995
1141
|
})
|
|
@@ -1054,7 +1200,8 @@ export class CommonDao<
|
|
|
1054
1200
|
dao.assignIdCreatedUpdated(row, opt)
|
|
1055
1201
|
const dbm = await dao.bmToDBM(row, opt)
|
|
1056
1202
|
dao.cfg.hooks!.beforeSave?.(dbm)
|
|
1057
|
-
|
|
1203
|
+
const storageRow = await dao.dbmToStorageRow(dbm)
|
|
1204
|
+
dbmsByTable[table].push(storageRow)
|
|
1058
1205
|
} else {
|
|
1059
1206
|
// Plural
|
|
1060
1207
|
input.rows.forEach(bm => dao.assignIdCreatedUpdated(bm, opt))
|
|
@@ -1062,7 +1209,8 @@ export class CommonDao<
|
|
|
1062
1209
|
if (dao.cfg.hooks!.beforeSave) {
|
|
1063
1210
|
dbms.forEach(dbm => dao.cfg.hooks!.beforeSave!(dbm))
|
|
1064
1211
|
}
|
|
1065
|
-
|
|
1212
|
+
const storageRows = await dao.dbmsToStorageRows(dbms)
|
|
1213
|
+
dbmsByTable[table].push(...storageRows)
|
|
1066
1214
|
}
|
|
1067
1215
|
})
|
|
1068
1216
|
|
|
@@ -1197,4 +1345,4 @@ export type AnyDao = CommonDao<any>
|
|
|
1197
1345
|
* Used internally during compression/decompression so that DBM instances can
|
|
1198
1346
|
* carry their compressed payload alongside the original type shape.
|
|
1199
1347
|
*/
|
|
1200
|
-
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
|
|