@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/src/Columns.d.ts +12 -6
  3. package/dist/src/Columns.d.ts.map +1 -1
  4. package/dist/src/Columns.js +40 -65
  5. package/dist/src/Columns.js.map +1 -1
  6. package/dist/src/Db.d.ts.map +1 -1
  7. package/dist/src/Db.js +2 -5
  8. package/dist/src/Db.js.map +1 -1
  9. package/dist/src/Record.d.ts +19 -0
  10. package/dist/src/Record.d.ts.map +1 -1
  11. package/dist/src/Record.js +127 -45
  12. package/dist/src/Record.js.map +1 -1
  13. package/dist/src/Table.d.ts +18 -1
  14. package/dist/src/Table.d.ts.map +1 -1
  15. package/dist/src/encryption/EncryptionRecordHooks.d.ts +9 -4
  16. package/dist/src/encryption/EncryptionRecordHooks.d.ts.map +1 -1
  17. package/dist/src/encryption/EncryptionRecordHooks.js +65 -42
  18. package/dist/src/encryption/EncryptionRecordHooks.js.map +1 -1
  19. package/dist/test/DateTimeFieldReadOnFirstRead.test.d.ts +2 -0
  20. package/dist/test/DateTimeFieldReadOnFirstRead.test.d.ts.map +1 -0
  21. package/dist/test/DateTimeFieldReadOnFirstRead.test.js +238 -0
  22. package/dist/test/DateTimeFieldReadOnFirstRead.test.js.map +1 -0
  23. package/dist/test/HydrationBenchmark.test.d.ts +2 -0
  24. package/dist/test/HydrationBenchmark.test.d.ts.map +1 -0
  25. package/dist/test/HydrationBenchmark.test.js +299 -0
  26. package/dist/test/HydrationBenchmark.test.js.map +1 -0
  27. package/dist/test/SyncRowHydration.test.d.ts +2 -0
  28. package/dist/test/SyncRowHydration.test.d.ts.map +1 -0
  29. package/dist/test/SyncRowHydration.test.js +344 -0
  30. package/dist/test/SyncRowHydration.test.js.map +1 -0
  31. package/package.json +2 -2
  32. package/src/Columns.ts +14 -7
  33. package/src/Db.ts +3 -4
  34. package/src/Record.ts +151 -17
  35. package/src/Table.ts +21 -1
  36. package/src/encryption/EncryptionRecordHooks.ts +48 -17
  37. package/test/DateTimeFieldReadOnFirstRead.test.ts +162 -0
  38. package/test/HydrationBenchmark.test.ts +209 -0
  39. 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.1",
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": "9c87e1ee2e65c4b2ffcf5308cbab21b88d53365c"
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
- async deserialize(serializedFieldValue: boolean | null): Promise<boolean | null> {
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
- async deserialize(serializedFieldValue: Date | null): Promise<moment.Moment | null> {
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
- async deserialize(serializedFieldValue: string): Promise<T | null> {
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
- async deserialize(serializedFieldValue: string): Promise<ReferenceArray<T> | null> {
251
- let ids = (await super.deserialize(serializedFieldValue)) as string[] | null;
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
- async deserialize(serializedFieldValue: string): Promise<Reference<T> | null> {
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
- async deserialize(serializedFieldValue: string, serializedRecord: any): Promise<Reference<T> | null> {
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
- const recordSerializer = new RecordSerializer(table);
754
- const records = await Promise.all(
755
- serializedRecords.map(async (serializedRecord) => recordSerializer.deserialize(serializedRecord))
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
- const { EncryptionRecordHooks } = await import('./encryption/EncryptionRecordHooks');
104
- const prepared = await new EncryptionRecordHooks().onDeserialize(this.table, serializedRecord);
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
- const deserialized: any = {};
107
- const fieldSerializer = new FieldSerializer(this.table);
108
- const omittedFields: string[] = [];
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 serializedFieldValue = prepared[columnName];
111
- try {
112
- const { fieldPropertyName, fieldValue } = await fieldSerializer.deserialize(
113
- columnName,
114
- serializedFieldValue,
115
- prepared
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
- deserialized[fieldPropertyName] = fieldValue;
118
- } catch (MissingFieldError) {
119
- omittedFields.push(columnName);
188
+ continue;
120
189
  }
190
+
191
+ record[propertyName] = fieldValue;
121
192
  }
122
193
 
123
- if (omittedFields.length > 0) {
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
- return deserialized;
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