@proteinjs/db 1.54.1 → 1.54.2
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/CHANGELOG.md +11 -0
- package/dist/src/Columns.d.ts +12 -6
- package/dist/src/Columns.d.ts.map +1 -1
- package/dist/src/Columns.js +40 -65
- package/dist/src/Columns.js.map +1 -1
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +2 -5
- package/dist/src/Db.js.map +1 -1
- package/dist/src/Record.d.ts +19 -0
- package/dist/src/Record.d.ts.map +1 -1
- package/dist/src/Record.js +127 -45
- package/dist/src/Record.js.map +1 -1
- package/dist/src/Table.d.ts +18 -1
- package/dist/src/Table.d.ts.map +1 -1
- package/dist/src/encryption/EncryptionRecordHooks.d.ts +9 -4
- package/dist/src/encryption/EncryptionRecordHooks.d.ts.map +1 -1
- package/dist/src/encryption/EncryptionRecordHooks.js +65 -42
- package/dist/src/encryption/EncryptionRecordHooks.js.map +1 -1
- package/dist/test/DateTimeFieldReadOnFirstRead.test.d.ts +2 -0
- package/dist/test/DateTimeFieldReadOnFirstRead.test.d.ts.map +1 -0
- package/dist/test/DateTimeFieldReadOnFirstRead.test.js +238 -0
- package/dist/test/DateTimeFieldReadOnFirstRead.test.js.map +1 -0
- package/dist/test/HydrationBenchmark.test.d.ts +2 -0
- package/dist/test/HydrationBenchmark.test.d.ts.map +1 -0
- package/dist/test/HydrationBenchmark.test.js +299 -0
- package/dist/test/HydrationBenchmark.test.js.map +1 -0
- package/dist/test/SyncRowHydration.test.d.ts +2 -0
- package/dist/test/SyncRowHydration.test.d.ts.map +1 -0
- package/dist/test/SyncRowHydration.test.js +344 -0
- package/dist/test/SyncRowHydration.test.js.map +1 -0
- package/package.json +2 -2
- package/src/Columns.ts +14 -7
- package/src/Db.ts +3 -4
- package/src/Record.ts +151 -17
- package/src/Table.ts +21 -1
- package/src/encryption/EncryptionRecordHooks.ts +48 -17
- package/test/DateTimeFieldReadOnFirstRead.test.ts +162 -0
- package/test/HydrationBenchmark.test.ts +209 -0
- package/test/SyncRowHydration.test.ts +249 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __extends = (this && this.__extends) || (function () {
|
|
3
|
+
var extendStatics = function (d, b) {
|
|
4
|
+
extendStatics = Object.setPrototypeOf ||
|
|
5
|
+
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
6
|
+
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
|
7
|
+
return extendStatics(d, b);
|
|
8
|
+
};
|
|
9
|
+
return function (d, b) {
|
|
10
|
+
if (typeof b !== "function" && b !== null)
|
|
11
|
+
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
|
12
|
+
extendStatics(d, b);
|
|
13
|
+
function __() { this.constructor = d; }
|
|
14
|
+
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
15
|
+
};
|
|
16
|
+
})();
|
|
17
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
18
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
19
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
20
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
21
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
22
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
23
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
27
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
28
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
29
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
30
|
+
function step(op) {
|
|
31
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
32
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
33
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
34
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
35
|
+
switch (op[0]) {
|
|
36
|
+
case 0: case 1: t = op; break;
|
|
37
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
38
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
39
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
40
|
+
default:
|
|
41
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
42
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
43
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
44
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
45
|
+
if (t[2]) _.ops.pop();
|
|
46
|
+
_.trys.pop(); continue;
|
|
47
|
+
}
|
|
48
|
+
op = body.call(thisArg, _);
|
|
49
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
50
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
var crypto_1 = require("crypto");
|
|
55
|
+
var reflection_1 = require("@proteinjs/reflection");
|
|
56
|
+
var Db_1 = require("../src/Db");
|
|
57
|
+
var TableWatcherRunner_1 = require("../src/TableWatcherRunner");
|
|
58
|
+
var Table_1 = require("../src/Table");
|
|
59
|
+
var Record_1 = require("../src/Record");
|
|
60
|
+
var Columns_1 = require("../src/Columns");
|
|
61
|
+
var DataKeyStore_1 = require("../src/encryption/DataKeyStore");
|
|
62
|
+
var EncryptionEnvelope_1 = require("../src/encryption/EncryptionEnvelope");
|
|
63
|
+
/** Where the loop stood when each cell was read: the row, and whether the sentinel had fired by then. */
|
|
64
|
+
var cellsRead = [];
|
|
65
|
+
var sentinelFired = false;
|
|
66
|
+
/**
|
|
67
|
+
* A column that notes, at each read, whether a microtask armed by the FIRST cell read has fired yet:
|
|
68
|
+
* a hydration that yields the loop between rows lets it fire mid-way; a synchronous one never does.
|
|
69
|
+
*/
|
|
70
|
+
var ObservingColumn = /** @class */ (function () {
|
|
71
|
+
function ObservingColumn(name) {
|
|
72
|
+
this.name = name;
|
|
73
|
+
}
|
|
74
|
+
ObservingColumn.prototype.deserialize = function (storedValue) {
|
|
75
|
+
if (cellsRead.length === 0) {
|
|
76
|
+
queueMicrotask(function () {
|
|
77
|
+
sentinelFired = true;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
cellsRead.push({ column: this.name, row: storedValue, sentinelFired: sentinelFired });
|
|
81
|
+
return storedValue;
|
|
82
|
+
};
|
|
83
|
+
return ObservingColumn;
|
|
84
|
+
}());
|
|
85
|
+
var DocTable = /** @class */ (function (_super) {
|
|
86
|
+
__extends(DocTable, _super);
|
|
87
|
+
function DocTable() {
|
|
88
|
+
var _this = _super !== null && _super.apply(this, arguments) || this;
|
|
89
|
+
_this.name = 'hydration_doc';
|
|
90
|
+
_this.columns = (0, Record_1.withRecordColumns)({
|
|
91
|
+
title: new Columns_1.StringColumn('title'),
|
|
92
|
+
first: new ObservingColumn('first'),
|
|
93
|
+
second: new ObservingColumn('second'),
|
|
94
|
+
});
|
|
95
|
+
return _this;
|
|
96
|
+
}
|
|
97
|
+
return DocTable;
|
|
98
|
+
}(Table_1.Table));
|
|
99
|
+
var docTable = new DocTable();
|
|
100
|
+
var TestTransactionContextFactory = /** @class */ (function () {
|
|
101
|
+
function TestTransactionContextFactory() {
|
|
102
|
+
this.context = {};
|
|
103
|
+
}
|
|
104
|
+
TestTransactionContextFactory.prototype.getTransactionContext = function () {
|
|
105
|
+
return this.context;
|
|
106
|
+
};
|
|
107
|
+
TestTransactionContextFactory.prototype.runInContext = function (context, fn) {
|
|
108
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
109
|
+
var previous;
|
|
110
|
+
return __generator(this, function (_a) {
|
|
111
|
+
switch (_a.label) {
|
|
112
|
+
case 0:
|
|
113
|
+
previous = this.context;
|
|
114
|
+
this.context = context;
|
|
115
|
+
_a.label = 1;
|
|
116
|
+
case 1:
|
|
117
|
+
_a.trys.push([1, , 3, 4]);
|
|
118
|
+
return [4 /*yield*/, fn()];
|
|
119
|
+
case 2: return [2 /*return*/, _a.sent()];
|
|
120
|
+
case 3:
|
|
121
|
+
this.context = previous;
|
|
122
|
+
return [7 /*endfinally*/];
|
|
123
|
+
case 4: return [2 /*return*/];
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
return TestTransactionContextFactory;
|
|
129
|
+
}());
|
|
130
|
+
/** A driver that answers every query with the rows it was handed. */
|
|
131
|
+
var RowsDriver = /** @class */ (function () {
|
|
132
|
+
function RowsDriver(rows) {
|
|
133
|
+
this.rows = rows;
|
|
134
|
+
}
|
|
135
|
+
RowsDriver.prototype.getDbName = function () {
|
|
136
|
+
return 'test';
|
|
137
|
+
};
|
|
138
|
+
RowsDriver.prototype.createDbIfNotExists = function () {
|
|
139
|
+
return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
140
|
+
return [2 /*return*/];
|
|
141
|
+
}); });
|
|
142
|
+
};
|
|
143
|
+
RowsDriver.prototype.createDb = function () {
|
|
144
|
+
return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
145
|
+
return [2 /*return*/];
|
|
146
|
+
}); });
|
|
147
|
+
};
|
|
148
|
+
RowsDriver.prototype.dropDb = function () {
|
|
149
|
+
return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
150
|
+
return [2 /*return*/];
|
|
151
|
+
}); });
|
|
152
|
+
};
|
|
153
|
+
RowsDriver.prototype.getTableManager = function () {
|
|
154
|
+
throw new Error('not used by these tests');
|
|
155
|
+
};
|
|
156
|
+
RowsDriver.prototype.runQuery = function () {
|
|
157
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
158
|
+
return __generator(this, function (_a) {
|
|
159
|
+
return [2 /*return*/, this.rows];
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
RowsDriver.prototype.runDml = function () {
|
|
164
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
165
|
+
return __generator(this, function (_a) {
|
|
166
|
+
return [2 /*return*/, 0];
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
RowsDriver.prototype.runTransaction = function (fn) {
|
|
171
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
172
|
+
return __generator(this, function (_a) {
|
|
173
|
+
switch (_a.label) {
|
|
174
|
+
case 0: return [4 /*yield*/, fn({ id: 'transaction' })];
|
|
175
|
+
case 1: return [2 /*return*/, _a.sent()];
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
RowsDriver.prototype.getOperationDeadlineMs = function () {
|
|
181
|
+
return 1000;
|
|
182
|
+
};
|
|
183
|
+
return RowsDriver;
|
|
184
|
+
}());
|
|
185
|
+
var dbOver = function (rows) {
|
|
186
|
+
return new Db_1.Db(new RowsDriver(rows), function () { return docTable; }, new TestTransactionContextFactory(), true);
|
|
187
|
+
};
|
|
188
|
+
var storedRows = function (count) {
|
|
189
|
+
return Array.from({ length: count }, function (_, i) { return ({
|
|
190
|
+
id: "row-".concat(i),
|
|
191
|
+
created: new Date(1700000000000 + i),
|
|
192
|
+
updated: new Date(1700000000000 + i),
|
|
193
|
+
title: "title ".concat(i),
|
|
194
|
+
first: i,
|
|
195
|
+
second: i,
|
|
196
|
+
}); });
|
|
197
|
+
};
|
|
198
|
+
var objectCache = function () { return reflection_1.SourceRepository.get().objectCache; };
|
|
199
|
+
var originalWatchers;
|
|
200
|
+
// No reflection graph in a unit test: the watcher registry is empty by declaration.
|
|
201
|
+
beforeAll(function () {
|
|
202
|
+
originalWatchers = objectCache()['@proteinjs/db/TableWatcher'];
|
|
203
|
+
objectCache()['@proteinjs/db/TableWatcher'] = [];
|
|
204
|
+
TableWatcherRunner_1.TableWatcherRunner.tableWatcherMap = undefined;
|
|
205
|
+
});
|
|
206
|
+
afterAll(function () {
|
|
207
|
+
if (originalWatchers) {
|
|
208
|
+
objectCache()['@proteinjs/db/TableWatcher'] = originalWatchers;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
delete objectCache()['@proteinjs/db/TableWatcher'];
|
|
212
|
+
}
|
|
213
|
+
TableWatcherRunner_1.TableWatcherRunner.tableWatcherMap = undefined;
|
|
214
|
+
});
|
|
215
|
+
var keyFor = function (owner, version) { return ({
|
|
216
|
+
owner: owner,
|
|
217
|
+
version: version,
|
|
218
|
+
cipherKey: (0, crypto_1.randomBytes)(32),
|
|
219
|
+
indexKey: (0, crypto_1.randomBytes)(32),
|
|
220
|
+
}); };
|
|
221
|
+
describe('a result set hydrates in one synchronous pass when no row needs an await', function () {
|
|
222
|
+
beforeEach(function () {
|
|
223
|
+
cellsRead.length = 0;
|
|
224
|
+
sentinelFired = false;
|
|
225
|
+
});
|
|
226
|
+
it('a query over plain rows never yields the event loop between rows', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
227
|
+
var rows, records;
|
|
228
|
+
return __generator(this, function (_a) {
|
|
229
|
+
switch (_a.label) {
|
|
230
|
+
case 0:
|
|
231
|
+
rows = storedRows(200);
|
|
232
|
+
return [4 /*yield*/, dbOver(rows).query(docTable, {})];
|
|
233
|
+
case 1:
|
|
234
|
+
records = _a.sent();
|
|
235
|
+
expect(records).toHaveLength(200);
|
|
236
|
+
expect(records[199]).toMatchObject({ id: 'row-199', title: 'title 199', first: 199, second: 199 });
|
|
237
|
+
// Every cell of every row was read before the microtask armed by the first cell could fire.
|
|
238
|
+
expect(cellsRead).toHaveLength(400);
|
|
239
|
+
expect(cellsRead.filter(function (cell) { return cell.sentinelFired; }).map(function (cell) { return "".concat(cell.column, ":").concat(cell.row); })).toEqual([]);
|
|
240
|
+
expect(sentinelFired).toBe(true);
|
|
241
|
+
return [2 /*return*/];
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}); });
|
|
245
|
+
it('the serializer answers a plain result set with the array itself, not a promise', function () {
|
|
246
|
+
var rows = storedRows(3);
|
|
247
|
+
var records = new Record_1.RecordSerializer(docTable).deserializeAll(rows);
|
|
248
|
+
expect(Array.isArray(records)).toBe(true);
|
|
249
|
+
expect(records.map(function (record) { return record.title; })).toEqual(['title 0', 'title 1', 'title 2']);
|
|
250
|
+
});
|
|
251
|
+
it('a column whose deserialize answers with a promise still settles, in column order', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
252
|
+
var LaterColumn, LaterTable, laterTable, rows, settling, records;
|
|
253
|
+
return __generator(this, function (_a) {
|
|
254
|
+
switch (_a.label) {
|
|
255
|
+
case 0:
|
|
256
|
+
LaterColumn = /** @class */ (function () {
|
|
257
|
+
function LaterColumn(name) {
|
|
258
|
+
this.name = name;
|
|
259
|
+
}
|
|
260
|
+
LaterColumn.prototype.deserialize = function (storedValue) {
|
|
261
|
+
return Promise.resolve(storedValue === null ? null : storedValue * 2);
|
|
262
|
+
};
|
|
263
|
+
return LaterColumn;
|
|
264
|
+
}());
|
|
265
|
+
LaterTable = /** @class */ (function (_super) {
|
|
266
|
+
__extends(LaterTable, _super);
|
|
267
|
+
function LaterTable() {
|
|
268
|
+
var _this = _super !== null && _super.apply(this, arguments) || this;
|
|
269
|
+
_this.name = 'hydration_later';
|
|
270
|
+
_this.columns = (0, Record_1.withRecordColumns)({
|
|
271
|
+
title: new Columns_1.StringColumn('title'),
|
|
272
|
+
later: new LaterColumn('later'),
|
|
273
|
+
after: new Columns_1.StringColumn('after'),
|
|
274
|
+
});
|
|
275
|
+
return _this;
|
|
276
|
+
}
|
|
277
|
+
return LaterTable;
|
|
278
|
+
}(Table_1.Table));
|
|
279
|
+
laterTable = new LaterTable();
|
|
280
|
+
rows = [
|
|
281
|
+
{ id: 'a', created: new Date(), updated: new Date(), title: 'one', later: 21, after: 'x' },
|
|
282
|
+
{ id: 'b', created: new Date(), updated: new Date(), title: 'two', later: null, after: 'y' },
|
|
283
|
+
];
|
|
284
|
+
settling = new Record_1.RecordSerializer(laterTable).deserializeAll(rows);
|
|
285
|
+
expect(Array.isArray(settling)).toBe(false);
|
|
286
|
+
return [4 /*yield*/, settling];
|
|
287
|
+
case 1:
|
|
288
|
+
records = _a.sent();
|
|
289
|
+
expect(records.map(function (record) { return record.later; })).toEqual([42, null]);
|
|
290
|
+
expect(Object.keys(records[0])).toEqual(['id', 'created', 'updated', 'title', 'later', 'after']);
|
|
291
|
+
return [2 /*return*/];
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}); });
|
|
295
|
+
it('a row holding a ciphertext envelope takes the async road and decrypts; its plain neighbors ride along', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
296
|
+
var key, envelope, keyRead, rows, settling, records;
|
|
297
|
+
return __generator(this, function (_a) {
|
|
298
|
+
switch (_a.label) {
|
|
299
|
+
case 0:
|
|
300
|
+
key = keyFor('owner-1', 4);
|
|
301
|
+
envelope = new EncryptionEnvelope_1.EncryptionEnvelope();
|
|
302
|
+
keyRead = jest.spyOn(DataKeyStore_1.DataKeyStore.prototype, 'getKeyByVersion').mockResolvedValue(key);
|
|
303
|
+
_a.label = 1;
|
|
304
|
+
case 1:
|
|
305
|
+
_a.trys.push([1, , 3, 4]);
|
|
306
|
+
rows = storedRows(3);
|
|
307
|
+
rows[1].title = envelope.encrypt('the plaintext', key);
|
|
308
|
+
settling = new Record_1.RecordSerializer(docTable).deserializeAll(rows);
|
|
309
|
+
expect(Array.isArray(settling)).toBe(false);
|
|
310
|
+
return [4 /*yield*/, settling];
|
|
311
|
+
case 2:
|
|
312
|
+
records = _a.sent();
|
|
313
|
+
expect(records.map(function (record) { return record.title; })).toEqual(['title 0', 'the plaintext', 'title 2']);
|
|
314
|
+
expect(keyRead).toHaveBeenCalledTimes(1);
|
|
315
|
+
expect(keyRead).toHaveBeenCalledWith('owner-1', 4);
|
|
316
|
+
// The plain rows were hydrated in the one pass; the envelope row landed when its key read settled.
|
|
317
|
+
expect(cellsRead.map(function (cell) { return cell.row; })).toEqual([0, 0, 2, 2, 1, 1]);
|
|
318
|
+
return [3 /*break*/, 4];
|
|
319
|
+
case 3:
|
|
320
|
+
keyRead.mockRestore();
|
|
321
|
+
return [7 /*endfinally*/];
|
|
322
|
+
case 4: return [2 /*return*/];
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}); });
|
|
326
|
+
it('a stored column the table no longer declares is omitted; a column without deserialize passes its value through', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
327
|
+
var rows, record;
|
|
328
|
+
return __generator(this, function (_a) {
|
|
329
|
+
switch (_a.label) {
|
|
330
|
+
case 0:
|
|
331
|
+
rows = [
|
|
332
|
+
{ id: 'a', created: new Date(), updated: new Date(), title: 'one', first: 1, second: 1, retired: 'gone' },
|
|
333
|
+
];
|
|
334
|
+
return [4 /*yield*/, new Record_1.RecordSerializer(docTable).deserializeAll(rows)];
|
|
335
|
+
case 1:
|
|
336
|
+
record = (_a.sent())[0];
|
|
337
|
+
expect(record).toMatchObject({ id: 'a', title: 'one', first: 1, second: 1 });
|
|
338
|
+
expect('retired' in record).toBe(false);
|
|
339
|
+
return [2 /*return*/];
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}); });
|
|
343
|
+
});
|
|
344
|
+
//# sourceMappingURL=SyncRowHydration.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SyncRowHydration.test.js","sourceRoot":"","sources":["../../test/SyncRowHydration.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db",
|
|
3
|
-
"version": "1.54.
|
|
3
|
+
"version": "1.54.2",
|
|
4
4
|
"main": "./dist/generated/index.js",
|
|
5
5
|
"types": "./dist/generated/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"ts-jest": "29.1.1",
|
|
67
67
|
"typescript": "5.2.2"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "d90bd35b32b628c2b9cf9d8ddb16b4a2b9140f11"
|
|
70
70
|
}
|
package/src/Columns.ts
CHANGED
|
@@ -74,7 +74,7 @@ export class BooleanColumn implements Column<boolean | null, boolean | null> {
|
|
|
74
74
|
return null;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
deserialize(serializedFieldValue: boolean | null): boolean | null {
|
|
78
78
|
if (serializedFieldValue) {
|
|
79
79
|
return true;
|
|
80
80
|
}
|
|
@@ -95,6 +95,13 @@ export class DateColumn implements Column<Date, Date> {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
export class DateTimeColumn implements Column<moment.Moment | null, Date | null> {
|
|
98
|
+
/**
|
|
99
|
+
* The moment is built on the field's first read, not at hydration (`Column.deserializeOnFirstRead`):
|
|
100
|
+
* a row carries several of these and most are never read, and a moment costs more to build than
|
|
101
|
+
* the stored Date it is built from.
|
|
102
|
+
*/
|
|
103
|
+
readonly deserializeOnFirstRead = true;
|
|
104
|
+
|
|
98
105
|
constructor(
|
|
99
106
|
public name: string,
|
|
100
107
|
public options?: ColumnOptions
|
|
@@ -112,7 +119,7 @@ export class DateTimeColumn implements Column<moment.Moment | null, Date | null>
|
|
|
112
119
|
return fieldValue.toDate();
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
|
|
122
|
+
deserialize(serializedFieldValue: Date | null): moment.Moment | null {
|
|
116
123
|
if (serializedFieldValue === undefined || serializedFieldValue === null) {
|
|
117
124
|
return null;
|
|
118
125
|
}
|
|
@@ -186,7 +193,7 @@ export class ObjectColumn<T> extends StringColumn<T> {
|
|
|
186
193
|
return Serializer.serialize(fieldValue);
|
|
187
194
|
}
|
|
188
195
|
|
|
189
|
-
|
|
196
|
+
deserialize(serializedFieldValue: string): T | null {
|
|
190
197
|
if (serializedFieldValue === undefined || serializedFieldValue == null) {
|
|
191
198
|
return null;
|
|
192
199
|
}
|
|
@@ -247,8 +254,8 @@ export class ReferenceArrayColumn<T extends Record> extends ObjectColumn<Referen
|
|
|
247
254
|
return await super.serialize(returnIds as any);
|
|
248
255
|
}
|
|
249
256
|
|
|
250
|
-
|
|
251
|
-
let ids =
|
|
257
|
+
deserialize(serializedFieldValue: string): ReferenceArray<T> | null {
|
|
258
|
+
let ids = super.deserialize(serializedFieldValue) as string[] | null;
|
|
252
259
|
if (ids === null) {
|
|
253
260
|
ids = [];
|
|
254
261
|
}
|
|
@@ -315,7 +322,7 @@ export class ReferenceColumn<T extends Record> extends StringColumn<Reference<T>
|
|
|
315
322
|
return fieldValue._id;
|
|
316
323
|
}
|
|
317
324
|
|
|
318
|
-
|
|
325
|
+
deserialize(serializedFieldValue: string): Reference<T> | null {
|
|
319
326
|
const reference = new Reference(this.referenceTable, serializedFieldValue);
|
|
320
327
|
if (reference._id === null) {
|
|
321
328
|
return null;
|
|
@@ -487,7 +494,7 @@ export class DynamicReferenceColumn<T extends Record> extends StringColumn<Refer
|
|
|
487
494
|
return fieldValue._id;
|
|
488
495
|
}
|
|
489
496
|
|
|
490
|
-
|
|
497
|
+
deserialize(serializedFieldValue: string, serializedRecord: any): Reference<T> | null {
|
|
491
498
|
const reference = new Reference('', serializedFieldValue);
|
|
492
499
|
if (reference._id === null) {
|
|
493
500
|
return null;
|
package/src/Db.ts
CHANGED
|
@@ -750,10 +750,9 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
750
750
|
const generateQuery = (config: DbDriverQueryStatementConfig) =>
|
|
751
751
|
qb.toSql(this.statementConfigFactory.getStatementConfig(config));
|
|
752
752
|
const serializedRecords = await this.dbDriver.runQuery(generateQuery, this.transactionForDriver());
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
);
|
|
753
|
+
// One pass over the result set — synchronous when no row needs an await (see
|
|
754
|
+
// `RecordSerializer.deserializeAll`): no promise per row, no yield of the loop between rows.
|
|
755
|
+
const records = await new RecordSerializer<T>(table).deserializeAll(serializedRecords);
|
|
757
756
|
await this.preloadReferences(records, options);
|
|
758
757
|
return records;
|
|
759
758
|
}
|
package/src/Record.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Logger } from '@proteinjs/logger';
|
|
|
2
2
|
import { DateTimeColumn, UuidColumn } from './Columns';
|
|
3
3
|
import { Column, Table, Columns } from './Table';
|
|
4
4
|
import { moment } from './opt/moment';
|
|
5
|
+
import type { EncryptionRecordHooks } from './encryption/EncryptionRecordHooks';
|
|
5
6
|
|
|
6
7
|
export interface Record {
|
|
7
8
|
id: string;
|
|
@@ -51,10 +52,26 @@ export type SerializedRecord = { [columnName: string]: any };
|
|
|
51
52
|
* (see `EncryptionRecordHooks`). Resolved by `Db` for writes touching encrypted columns. */
|
|
52
53
|
export type RecordEncryptionContext = { keyOwner: string };
|
|
53
54
|
|
|
55
|
+
/** One column's seat in a hydration: the property the record carries it under, and the column. */
|
|
56
|
+
interface HydrationSlot {
|
|
57
|
+
propertyName: string;
|
|
58
|
+
column: Column<any, any>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* What a hydration resolves ONCE per serializer, never per row: the decryption hook and every
|
|
63
|
+
* column's seat, keyed by the name a stored row carries it under.
|
|
64
|
+
*/
|
|
65
|
+
interface HydrationPlan {
|
|
66
|
+
hooks: EncryptionRecordHooks;
|
|
67
|
+
slotsByColumnName: { [columnName: string]: HydrationSlot | undefined };
|
|
68
|
+
}
|
|
69
|
+
|
|
54
70
|
export class RecordSerializer<T extends Record> {
|
|
55
71
|
private logger = new Logger({ name: this.constructor.name });
|
|
56
72
|
private table: Table<T>;
|
|
57
73
|
private encryptionContext?: RecordEncryptionContext;
|
|
74
|
+
private plan?: HydrationPlan;
|
|
58
75
|
|
|
59
76
|
constructor(table: Table<T>, encryptionContext?: RecordEncryptionContext) {
|
|
60
77
|
this.table = table;
|
|
@@ -97,35 +114,152 @@ export class RecordSerializer<T extends Record> {
|
|
|
97
114
|
return serialized;
|
|
98
115
|
}
|
|
99
116
|
|
|
117
|
+
/**
|
|
118
|
+
* A result set's rows → records, in ONE pass. A row whose hook and columns all answer directly
|
|
119
|
+
* is hydrated synchronously, and when every row did the array returns synchronously: no promise
|
|
120
|
+
* per row, no yield of the event loop between rows. A row that needs an await — a ciphertext
|
|
121
|
+
* envelope to decrypt, a column whose `deserialize` answers with a promise — settles through one
|
|
122
|
+
* promise for the whole set. The hook and the column seats are resolved once per serializer.
|
|
123
|
+
*/
|
|
124
|
+
deserializeAll(serializedRecords: SerializedRecord[]): T[] | Promise<T[]> {
|
|
125
|
+
const plan = this.hydrationPlan();
|
|
126
|
+
const records: (T | Promise<T>)[] = new Array(serializedRecords.length);
|
|
127
|
+
let settles = false;
|
|
128
|
+
for (let i = 0; i < serializedRecords.length; i++) {
|
|
129
|
+
const record = this.hydrate(plan, serializedRecords[i]);
|
|
130
|
+
if (this.isPromise(record)) {
|
|
131
|
+
settles = true;
|
|
132
|
+
}
|
|
133
|
+
records[i] = record;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return settles ? Promise.all(records) : (records as T[]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** One row → one record (`deserializeAll` is the door for a result set). */
|
|
100
140
|
async deserialize(serializedRecord: SerializedRecord): Promise<T> {
|
|
141
|
+
return await this.hydrate(this.hydrationPlan(), serializedRecord);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private hydrate(plan: HydrationPlan, serializedRecord: SerializedRecord): T | Promise<T> {
|
|
101
145
|
// The transparent decryption seam: ciphertext envelopes decrypt (self-describing — the
|
|
102
|
-
// envelope names its key), framework companion columns drop out of the result.
|
|
103
|
-
|
|
104
|
-
const prepared =
|
|
146
|
+
// envelope names its key), framework companion columns drop out of the result. The hook
|
|
147
|
+
// answers directly for a row holding no envelope.
|
|
148
|
+
const prepared = plan.hooks.onDeserialize(this.table, serializedRecord);
|
|
149
|
+
if (this.isPromise(prepared)) {
|
|
150
|
+
return prepared.then((row) => this.hydratePrepared(plan, row));
|
|
151
|
+
}
|
|
105
152
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
153
|
+
return this.hydratePrepared(plan, prepared);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private hydratePrepared(plan: HydrationPlan, prepared: SerializedRecord): T | Promise<T> {
|
|
157
|
+
const record: any = {};
|
|
158
|
+
let settling: Promise<void>[] | undefined;
|
|
159
|
+
let omittedFields: string[] | undefined;
|
|
109
160
|
for (const columnName in prepared) {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
161
|
+
const slot = plan.slotsByColumnName[columnName];
|
|
162
|
+
if (!slot) {
|
|
163
|
+
// a column exists in the db that is no longer defined in Table.columns
|
|
164
|
+
(omittedFields ??= []).push(columnName);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { propertyName, column } = slot;
|
|
169
|
+
const storedValue = prepared[columnName];
|
|
170
|
+
if (!column.deserialize) {
|
|
171
|
+
record[propertyName] = storedValue;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (column.deserializeOnFirstRead && storedValue !== null && storedValue !== undefined) {
|
|
176
|
+
this.defineFieldReadOnFirstRead(record, propertyName, column, storedValue);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const fieldValue = column.deserialize(storedValue, prepared);
|
|
181
|
+
if (this.isPromise(fieldValue)) {
|
|
182
|
+
record[propertyName] = undefined; // seats the property in column order; the value lands when it settles
|
|
183
|
+
(settling ??= []).push(
|
|
184
|
+
fieldValue.then((value) => {
|
|
185
|
+
record[propertyName] = value;
|
|
186
|
+
})
|
|
116
187
|
);
|
|
117
|
-
|
|
118
|
-
} catch (MissingFieldError) {
|
|
119
|
-
omittedFields.push(columnName);
|
|
188
|
+
continue;
|
|
120
189
|
}
|
|
190
|
+
|
|
191
|
+
record[propertyName] = fieldValue;
|
|
121
192
|
}
|
|
122
193
|
|
|
123
|
-
if (omittedFields
|
|
194
|
+
if (omittedFields) {
|
|
124
195
|
// expected when passing a base table into the query api
|
|
125
196
|
this.logger.debug({ message: `Fields were omitted during deserialization`, obj: { omittedFields } });
|
|
126
197
|
}
|
|
127
198
|
|
|
128
|
-
|
|
199
|
+
if (settling) {
|
|
200
|
+
return Promise.all(settling).then(() => record as T);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return record as T;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The field as an own, enumerable property whose value is deserialized on its first read, once
|
|
208
|
+
* (`Column.deserializeOnFirstRead`); a write replaces it like any other field.
|
|
209
|
+
*/
|
|
210
|
+
private defineFieldReadOnFirstRead(
|
|
211
|
+
record: any,
|
|
212
|
+
propertyName: string,
|
|
213
|
+
column: Column<any, any>,
|
|
214
|
+
storedValue: unknown
|
|
215
|
+
): void {
|
|
216
|
+
let value: unknown;
|
|
217
|
+
let read = false;
|
|
218
|
+
Object.defineProperty(record, propertyName, {
|
|
219
|
+
enumerable: true,
|
|
220
|
+
configurable: true,
|
|
221
|
+
get() {
|
|
222
|
+
if (!read) {
|
|
223
|
+
value = column.deserialize!(storedValue, undefined);
|
|
224
|
+
read = true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return value;
|
|
228
|
+
},
|
|
229
|
+
set(next: unknown) {
|
|
230
|
+
value = next;
|
|
231
|
+
read = true;
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private hydrationPlan(): HydrationPlan {
|
|
237
|
+
if (this.plan) {
|
|
238
|
+
return this.plan;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Required at call time — this module sits below the encryption machinery in the package's
|
|
242
|
+
// module graph (the seam `Table.ts` uses for the same reason).
|
|
243
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
244
|
+
const { EncryptionRecordHooks } =
|
|
245
|
+
require('./encryption/EncryptionRecordHooks') as typeof import('./encryption/EncryptionRecordHooks');
|
|
246
|
+
const slotsByColumnName: HydrationPlan['slotsByColumnName'] = {};
|
|
247
|
+
const columns: { [propertyName: string]: Column<any, any> } = this.table.columns;
|
|
248
|
+
// Seated by column name, then by property name — a stored key that is a property name
|
|
249
|
+
// resolves to that property's column first, as it always has.
|
|
250
|
+
for (const propertyName in columns) {
|
|
251
|
+
slotsByColumnName[columns[propertyName].name] = { propertyName, column: columns[propertyName] };
|
|
252
|
+
}
|
|
253
|
+
for (const propertyName in columns) {
|
|
254
|
+
slotsByColumnName[propertyName] = { propertyName, column: columns[propertyName] };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.plan = { hooks: new EncryptionRecordHooks(), slotsByColumnName };
|
|
258
|
+
return this.plan;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private isPromise<V>(value: V | Promise<V>): value is Promise<V> {
|
|
262
|
+
return typeof (value as { then?: unknown } | null | undefined)?.then === 'function';
|
|
129
263
|
}
|
|
130
264
|
}
|
|
131
265
|
|