idb-ts 3.11.1 → 3.12.0

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/lib/index.js CHANGED
@@ -9,57 +9,203 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.KeyGenerators = exports.RetentionPolicy = exports.Validate = exports.Index = exports.DataClass = exports.CompositeKeyPath = exports.KeyPath = exports.Database = void 0;
12
+ exports.KeyGenerators = exports.Database = void 0;
13
+ exports.KeyPath = KeyPath;
14
+ exports.CompositeKeyPath = CompositeKeyPath;
15
+ exports.DataClass = DataClass;
16
+ exports.Index = Index;
17
+ exports.Validate = Validate;
18
+ exports.RetentionPolicy = RetentionPolicy;
13
19
  require("reflect-metadata");
20
+ class FieldQueryBuilder {
21
+ constructor(parent, field) {
22
+ this.parent = parent;
23
+ this.field = field;
24
+ }
25
+ equals(value) {
26
+ this.parent.appendCondition(this.field, 'equals', value);
27
+ this.parent.clearCurrentField();
28
+ return this.parent;
29
+ }
30
+ gt(value) {
31
+ this.parent.appendCondition(this.field, 'gt', value);
32
+ this.parent.clearCurrentField();
33
+ return this.parent;
34
+ }
35
+ gte(value) {
36
+ this.parent.appendCondition(this.field, 'gte', value);
37
+ this.parent.clearCurrentField();
38
+ return this.parent;
39
+ }
40
+ lt(value) {
41
+ this.parent.appendCondition(this.field, 'lt', value);
42
+ this.parent.clearCurrentField();
43
+ return this.parent;
44
+ }
45
+ lte(value) {
46
+ this.parent.appendCondition(this.field, 'lte', value);
47
+ this.parent.clearCurrentField();
48
+ return this.parent;
49
+ }
50
+ startsWith(value) {
51
+ this.parent.appendCondition(this.field, 'startsWith', value);
52
+ this.parent.clearCurrentField();
53
+ return this.parent;
54
+ }
55
+ endsWith(value) {
56
+ this.parent.appendCondition(this.field, 'endsWith', value);
57
+ this.parent.clearCurrentField();
58
+ return this.parent;
59
+ }
60
+ contains(value) {
61
+ this.parent.appendCondition(this.field, 'contains', value);
62
+ this.parent.clearCurrentField();
63
+ return this.parent;
64
+ }
65
+ matches(value) {
66
+ this.parent.appendCondition(this.field, 'matches', value);
67
+ this.parent.clearCurrentField();
68
+ return this.parent;
69
+ }
70
+ between(start, end) {
71
+ this.parent.appendCondition(this.field, 'between', [start, end]);
72
+ this.parent.clearCurrentField();
73
+ return this.parent;
74
+ }
75
+ notBetween(start, end) {
76
+ this.parent.appendCondition(this.field, 'notBetween', [start, end]);
77
+ this.parent.clearCurrentField();
78
+ return this.parent;
79
+ }
80
+ in(values) {
81
+ this.parent.appendCondition(this.field, 'in', values);
82
+ this.parent.clearCurrentField();
83
+ return this.parent;
84
+ }
85
+ notIn(values) {
86
+ this.parent.appendCondition(this.field, 'notIn', values);
87
+ this.parent.clearCurrentField();
88
+ return this.parent;
89
+ }
90
+ containsAny(values) {
91
+ this.parent.appendCondition(this.field, 'containsAny', values);
92
+ this.parent.clearCurrentField();
93
+ return this.parent;
94
+ }
95
+ containsAll(values) {
96
+ this.parent.appendCondition(this.field, 'containsAll', values);
97
+ this.parent.clearCurrentField();
98
+ return this.parent;
99
+ }
100
+ and(field) {
101
+ this.parent.clearCurrentField();
102
+ return this.parent.and(field);
103
+ }
104
+ or() {
105
+ this.parent.clearCurrentField();
106
+ return this.parent.or();
107
+ }
108
+ }
14
109
  class QueryBuilder {
15
- constructor(db, storeName) {
16
- this.conditions = [];
110
+ constructor(db, storeName, transaction) {
111
+ this.clauses = [];
17
112
  this.orderDirection = 'asc';
113
+ this.pendingConnector = 'and';
18
114
  this.db = db;
19
115
  this.storeName = storeName;
116
+ this.transaction = transaction;
117
+ }
118
+ where(fieldOrBuilder) {
119
+ if (typeof fieldOrBuilder === 'function') {
120
+ this.clearCurrentField();
121
+ return this.addNestedGroup(fieldOrBuilder);
122
+ }
123
+ this.currentField = fieldOrBuilder;
124
+ return new FieldQueryBuilder(this, fieldOrBuilder);
125
+ }
126
+ and(fieldOrBuilder) {
127
+ return this.where(fieldOrBuilder);
20
128
  }
21
- where(field) {
22
- this.currentField = field;
129
+ or() {
130
+ this.pendingConnector = 'or';
23
131
  return this;
24
132
  }
25
- and(field) {
26
- this.currentField = field;
133
+ addNestedGroup(builder) {
134
+ var _a;
135
+ const nested = new QueryBuilder(this.db, this.storeName, this.transaction);
136
+ const returned = (_a = builder(nested)) !== null && _a !== void 0 ? _a : nested;
137
+ const clauses = returned instanceof QueryBuilder ? returned.clauses : nested.clauses;
138
+ this.appendClause({ kind: 'group', clauses });
27
139
  return this;
28
140
  }
29
- equals(value) {
30
- if (!this.currentField)
31
- throw new Error('No field specified for equals');
32
- this.conditions.push({ field: this.currentField, op: 'equals', value });
141
+ clearCurrentField() {
33
142
  this.currentField = undefined;
34
- return this;
35
143
  }
36
- gt(value) {
37
- if (!this.currentField)
38
- throw new Error('No field specified for gt');
39
- this.conditions.push({ field: this.currentField, op: 'gt', value });
144
+ appendCondition(field, op, value) {
145
+ this.appendClause({ kind: 'condition', field, op, value });
146
+ }
147
+ appendClause(clause) {
148
+ const connector = this.clauses.length === 0 ? null : this.pendingConnector;
149
+ this.clauses.push(Object.assign(Object.assign({}, clause), { connector }));
150
+ this.pendingConnector = 'and';
151
+ }
152
+ requireCurrentField(operation) {
153
+ if (!this.currentField) {
154
+ throw new Error(`No field specified for ${operation}`);
155
+ }
156
+ const field = this.currentField;
40
157
  this.currentField = undefined;
158
+ return field;
159
+ }
160
+ addCondition(op, value) {
161
+ const field = this.requireCurrentField(op);
162
+ this.appendCondition(field, op, value);
41
163
  return this;
42
164
  }
165
+ equals(value) {
166
+ return this.addCondition('equals', value);
167
+ }
168
+ gt(value) {
169
+ return this.addCondition('gt', value);
170
+ }
43
171
  gte(value) {
44
- if (!this.currentField)
45
- throw new Error('No field specified for gte');
46
- this.conditions.push({ field: this.currentField, op: 'gte', value });
47
- this.currentField = undefined;
48
- return this;
172
+ return this.addCondition('gte', value);
49
173
  }
50
174
  lt(value) {
51
- if (!this.currentField)
52
- throw new Error('No field specified for lt');
53
- this.conditions.push({ field: this.currentField, op: 'lt', value });
54
- this.currentField = undefined;
55
- return this;
175
+ return this.addCondition('lt', value);
56
176
  }
57
177
  lte(value) {
58
- if (!this.currentField)
59
- throw new Error('No field specified for lte');
60
- this.conditions.push({ field: this.currentField, op: 'lte', value });
61
- this.currentField = undefined;
62
- return this;
178
+ return this.addCondition('lte', value);
179
+ }
180
+ startsWith(value) {
181
+ return this.addCondition('startsWith', value);
182
+ }
183
+ endsWith(value) {
184
+ return this.addCondition('endsWith', value);
185
+ }
186
+ contains(value) {
187
+ return this.addCondition('contains', value);
188
+ }
189
+ matches(value) {
190
+ return this.addCondition('matches', value);
191
+ }
192
+ between(start, end) {
193
+ return this.addCondition('between', [start, end]);
194
+ }
195
+ notBetween(start, end) {
196
+ return this.addCondition('notBetween', [start, end]);
197
+ }
198
+ ['in'](values) {
199
+ return this.addCondition('in', values);
200
+ }
201
+ notIn(values) {
202
+ return this.addCondition('notIn', values);
203
+ }
204
+ containsAny(values) {
205
+ return this.addCondition('containsAny', values);
206
+ }
207
+ containsAll(values) {
208
+ return this.addCondition('containsAll', values);
63
209
  }
64
210
  orderBy(field, direction = 'asc') {
65
211
  this.orderField = field;
@@ -83,94 +229,226 @@ class QueryBuilder {
83
229
  this.rangeEnd = end;
84
230
  return this;
85
231
  }
86
- execute() {
232
+ groupBy(field) {
233
+ this.groupField = field;
234
+ return this;
235
+ }
236
+ loadCandidates() {
87
237
  return __awaiter(this, void 0, void 0, function* () {
238
+ const store = this.transaction
239
+ ? this.transaction.objectStore(this.storeName)
240
+ : this.db
241
+ .transaction(this.storeName, 'readonly')
242
+ .objectStore(this.storeName);
243
+ const request = this.createReadRequest(store);
88
244
  return new Promise((resolve, reject) => {
89
- const tx = this.db.transaction(this.storeName, 'readonly');
90
- const store = tx.objectStore(this.storeName);
91
- let request;
92
- let results = [];
93
- if (this.indexName) {
94
- if (!store.indexNames.contains(this.indexName)) {
95
- reject(new Error(`Index '${this.indexName}' does not exist on ${this.storeName}`));
96
- return;
97
- }
98
- const index = store.index(this.indexName);
99
- let keyRange;
100
- if (this.rangeStart !== undefined && this.rangeEnd !== undefined) {
101
- keyRange = IDBKeyRange.bound(this.rangeStart, this.rangeEnd);
102
- }
103
- else if (this.rangeStart !== undefined) {
104
- keyRange = IDBKeyRange.lowerBound(this.rangeStart);
105
- }
106
- else if (this.rangeEnd !== undefined) {
107
- keyRange = IDBKeyRange.upperBound(this.rangeEnd);
108
- }
109
- request = index.openCursor(keyRange);
110
- }
111
- else {
112
- request = store.openCursor();
113
- }
114
- request.onsuccess = () => {
115
- const cursor = request.result;
116
- if (cursor) {
117
- let match = true;
118
- const value = cursor.value;
119
- for (const cond of this.conditions) {
120
- const val = value[cond.field];
121
- switch (cond.op) {
122
- case 'equals':
123
- match = match && val === cond.value;
124
- break;
125
- case 'gt':
126
- match = match && val > cond.value;
127
- break;
128
- case 'gte':
129
- match = match && val >= cond.value;
130
- break;
131
- case 'lt':
132
- match = match && val < cond.value;
133
- break;
134
- case 'lte':
135
- match = match && val <= cond.value;
136
- break;
137
- }
138
- }
139
- if (match)
140
- results.push(value);
141
- cursor.continue();
142
- }
143
- else {
144
- if (this.orderField) {
145
- results.sort((a, b) => {
146
- const va = a[this.orderField];
147
- const vb = b[this.orderField];
148
- if (va < vb)
149
- return this.orderDirection === 'asc' ? -1 : 1;
150
- if (va > vb)
151
- return this.orderDirection === 'asc' ? 1 : -1;
152
- return 0;
153
- });
154
- }
155
- if (this.offsetCount !== undefined)
156
- results = results.slice(this.offsetCount);
157
- if (this.limitCount !== undefined)
158
- results = results.slice(0, this.limitCount);
159
- resolve(results);
160
- }
161
- };
245
+ request.onsuccess = () => { var _a; return resolve((_a = request.result) !== null && _a !== void 0 ? _a : []); };
162
246
  request.onerror = () => reject(request.error);
163
247
  });
164
248
  });
165
249
  }
250
+ createReadRequest(store) {
251
+ if (!this.indexName) {
252
+ return store.getAll();
253
+ }
254
+ if (!store.indexNames.contains(this.indexName)) {
255
+ throw new Error(`Index '${this.indexName}' does not exist on ${this.storeName}`);
256
+ }
257
+ const index = store.index(this.indexName);
258
+ let keyRange;
259
+ if (this.rangeStart !== undefined && this.rangeEnd !== undefined) {
260
+ keyRange = IDBKeyRange.bound(this.rangeStart, this.rangeEnd);
261
+ }
262
+ else if (this.rangeStart !== undefined) {
263
+ keyRange = IDBKeyRange.lowerBound(this.rangeStart);
264
+ }
265
+ else if (this.rangeEnd !== undefined) {
266
+ keyRange = IDBKeyRange.upperBound(this.rangeEnd);
267
+ }
268
+ return keyRange ? index.getAll(keyRange) : index.getAll();
269
+ }
270
+ matchesClause(item, clause) {
271
+ if (clause.kind === 'group') {
272
+ return this.evaluateClauses(item, clause.clauses);
273
+ }
274
+ const value = item[clause.field];
275
+ switch (clause.op) {
276
+ case 'equals':
277
+ return value === clause.value;
278
+ case 'gt':
279
+ return value > clause.value;
280
+ case 'gte':
281
+ return value >= clause.value;
282
+ case 'lt':
283
+ return value < clause.value;
284
+ case 'lte':
285
+ return value <= clause.value;
286
+ case 'startsWith':
287
+ return (typeof value === 'string' && value.startsWith(String(clause.value)));
288
+ case 'endsWith':
289
+ return (typeof value === 'string' && value.endsWith(String(clause.value)));
290
+ case 'contains':
291
+ if (typeof value === 'string') {
292
+ return value.includes(String(clause.value));
293
+ }
294
+ return Array.isArray(value) && value.includes(clause.value);
295
+ case 'matches': {
296
+ const pattern = clause.value instanceof RegExp
297
+ ? new RegExp(clause.value.source, clause.value.flags)
298
+ : new RegExp(String(clause.value));
299
+ return typeof value === 'string' && pattern.test(value);
300
+ }
301
+ case 'between': {
302
+ const [start, end] = clause.value;
303
+ return value >= start && value <= end;
304
+ }
305
+ case 'notBetween': {
306
+ const [start, end] = clause.value;
307
+ return value < start || value > end;
308
+ }
309
+ case 'in':
310
+ return Array.isArray(clause.value) && clause.value.includes(value);
311
+ case 'notIn':
312
+ return Array.isArray(clause.value) && !clause.value.includes(value);
313
+ case 'containsAny':
314
+ return (Array.isArray(value) &&
315
+ Array.isArray(clause.value) &&
316
+ clause.value.some((entry) => value.includes(entry)));
317
+ case 'containsAll':
318
+ return (Array.isArray(value) &&
319
+ Array.isArray(clause.value) &&
320
+ clause.value.every((entry) => value.includes(entry)));
321
+ }
322
+ }
323
+ evaluateClauses(item, clauses) {
324
+ if (!clauses.length) {
325
+ return true;
326
+ }
327
+ let result = this.matchesClause(item, clauses[0]);
328
+ for (let index = 1; index < clauses.length; index += 1) {
329
+ const clause = clauses[index];
330
+ const matches = this.matchesClause(item, clause);
331
+ result =
332
+ clause.connector === 'or' ? result || matches : result && matches;
333
+ }
334
+ return result;
335
+ }
336
+ collectMatches() {
337
+ return __awaiter(this, void 0, void 0, function* () {
338
+ const candidates = yield this.loadCandidates();
339
+ return candidates.filter((item) => this.evaluateClauses(item, this.clauses));
340
+ });
341
+ }
342
+ sortResults(results) {
343
+ if (!this.orderField) {
344
+ return results;
345
+ }
346
+ return [...results].sort((left, right) => {
347
+ const leftValue = left[this.orderField];
348
+ const rightValue = right[this.orderField];
349
+ if (leftValue < rightValue)
350
+ return this.orderDirection === 'asc' ? -1 : 1;
351
+ if (leftValue > rightValue)
352
+ return this.orderDirection === 'asc' ? 1 : -1;
353
+ return 0;
354
+ });
355
+ }
356
+ applyPagination(results) {
357
+ let nextResults = results;
358
+ if (this.offsetCount !== undefined) {
359
+ nextResults = nextResults.slice(this.offsetCount);
360
+ }
361
+ if (this.limitCount !== undefined) {
362
+ nextResults = nextResults.slice(0, this.limitCount);
363
+ }
364
+ return nextResults;
365
+ }
366
+ aggregateValues(field, reducer) {
367
+ return __awaiter(this, void 0, void 0, function* () {
368
+ const results = yield this.collectMatches();
369
+ const values = field
370
+ ? results.map((item) => item[field])
371
+ : results;
372
+ return reducer(values);
373
+ });
374
+ }
375
+ aggregateGroupedCount(field) {
376
+ return __awaiter(this, void 0, void 0, function* () {
377
+ var _a;
378
+ const results = yield this.collectMatches();
379
+ const groups = new Map();
380
+ for (const item of results) {
381
+ const key = item[field];
382
+ groups.set(key, ((_a = groups.get(key)) !== null && _a !== void 0 ? _a : 0) + 1);
383
+ }
384
+ return Array.from(groups.entries())
385
+ .sort(([left], [right]) => {
386
+ if (left < right)
387
+ return -1;
388
+ if (left > right)
389
+ return 1;
390
+ return 0;
391
+ })
392
+ .map(([key, count]) => ({ [field]: key, count }));
393
+ });
394
+ }
395
+ execute() {
396
+ return __awaiter(this, void 0, void 0, function* () {
397
+ const results = yield this.collectMatches();
398
+ return this.applyPagination(this.sortResults(results));
399
+ });
400
+ }
401
+ count() {
402
+ return __awaiter(this, void 0, void 0, function* () {
403
+ if (this.groupField) {
404
+ return this.aggregateGroupedCount(this.groupField);
405
+ }
406
+ const results = yield this.collectMatches();
407
+ return results.length;
408
+ });
409
+ }
410
+ sum(field) {
411
+ return __awaiter(this, void 0, void 0, function* () {
412
+ const total = yield this.aggregateValues(field, (values) => values.reduce((accumulator, value) => accumulator + (Number(value) || 0), 0));
413
+ return total;
414
+ });
415
+ }
416
+ avg(field) {
417
+ return __awaiter(this, void 0, void 0, function* () {
418
+ const values = yield this.aggregateValues(field, (items) => items);
419
+ if (!values.length) {
420
+ return 0;
421
+ }
422
+ const total = values.reduce((accumulator, value) => accumulator + (Number(value) || 0), 0);
423
+ return total / values.length;
424
+ });
425
+ }
426
+ min(field) {
427
+ return __awaiter(this, void 0, void 0, function* () {
428
+ const values = yield this.aggregateValues(field, (items) => items);
429
+ if (!values.length) {
430
+ return null;
431
+ }
432
+ return values.reduce((currentMin, value) => value < currentMin ? value : currentMin);
433
+ });
434
+ }
435
+ max(field) {
436
+ return __awaiter(this, void 0, void 0, function* () {
437
+ const values = yield this.aggregateValues(field, (items) => items);
438
+ if (!values.length) {
439
+ return null;
440
+ }
441
+ return values.reduce((currentMax, value) => value > currentMax ? value : currentMax);
442
+ });
443
+ }
166
444
  }
167
445
  const INTERNAL_CREATED_AT_FIELD = '__idb_createdAt';
168
446
  const INTERNAL_UPDATED_AT_FIELD = '__idb_updatedAt';
169
447
  class KeyGenerators {
170
448
  static uuid() {
171
449
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
172
- const r = Math.random() * 16 | 0;
173
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
450
+ const r = (Math.random() * 16) | 0;
451
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
174
452
  return v.toString(16);
175
453
  });
176
454
  }
@@ -185,45 +463,47 @@ exports.KeyGenerators = KeyGenerators;
185
463
  function KeyPath(options) {
186
464
  return (target, propertyKey) => {
187
465
  const constructor = target.constructor;
188
- const existingKeypaths = Reflect.getMetadata("individual_keypaths", constructor) || [];
466
+ const existingKeypaths = Reflect.getMetadata('individual_keypaths', constructor) || [];
189
467
  existingKeypaths.push(propertyKey);
190
- Reflect.defineMetadata("individual_keypaths", existingKeypaths, constructor);
468
+ Reflect.defineMetadata('individual_keypaths', existingKeypaths, constructor);
191
469
  const metadata = {
192
470
  fields: propertyKey,
193
- options: options
471
+ options: options,
194
472
  };
195
- Reflect.defineMetadata("keypath", metadata, constructor);
473
+ Reflect.defineMetadata('keypath', metadata, constructor);
196
474
  };
197
475
  }
198
- exports.KeyPath = KeyPath;
199
476
  function CompositeKeyPath(fields, options) {
200
477
  return (target) => {
201
478
  const metadata = {
202
479
  fields: fields,
203
- options: options
480
+ options: options,
204
481
  };
205
- Reflect.defineMetadata("keypath", metadata, target);
482
+ Reflect.defineMetadata('keypath', metadata, target);
206
483
  };
207
484
  }
208
- exports.CompositeKeyPath = CompositeKeyPath;
209
485
  function Index(options) {
210
486
  return (target, propertyKey) => {
211
487
  const constructor = target.constructor;
212
- const existing = Reflect.getMetadata("indexes", constructor) || [];
213
- const nextIndexes = [...existing, { field: propertyKey, options }];
214
- Reflect.defineMetadata("indexes", nextIndexes, constructor);
488
+ const existing = Reflect.getMetadata('indexes', constructor) || [];
489
+ const nextIndexes = [
490
+ ...existing,
491
+ { field: propertyKey, options },
492
+ ];
493
+ Reflect.defineMetadata('indexes', nextIndexes, constructor);
215
494
  };
216
495
  }
217
- exports.Index = Index;
218
496
  function Validate(predicate, message) {
219
497
  return (target, propertyKey) => {
220
498
  const constructor = target.constructor;
221
499
  const existing = Reflect.getMetadata('validators', constructor) || [];
222
- const nextRules = [...existing, { field: propertyKey, predicate, message }];
500
+ const nextRules = [
501
+ ...existing,
502
+ { field: propertyKey, predicate, message },
503
+ ];
223
504
  Reflect.defineMetadata('validators', nextRules, constructor);
224
505
  };
225
506
  }
226
- exports.Validate = Validate;
227
507
  function RetentionPolicy(options) {
228
508
  return (target) => {
229
509
  var _a, _b;
@@ -233,28 +513,26 @@ function RetentionPolicy(options) {
233
513
  const metadata = {
234
514
  seconds: options.seconds,
235
515
  enabled: (_a = options.enabled) !== null && _a !== void 0 ? _a : true,
236
- field: (_b = options.field) !== null && _b !== void 0 ? _b : INTERNAL_CREATED_AT_FIELD
516
+ field: (_b = options.field) !== null && _b !== void 0 ? _b : INTERNAL_CREATED_AT_FIELD,
237
517
  };
238
518
  Reflect.defineMetadata('retention_policy', metadata, target);
239
519
  };
240
520
  }
241
- exports.RetentionPolicy = RetentionPolicy;
242
521
  function DataClass(options = {}) {
243
522
  return (target) => {
244
- const keyPathMetadata = Reflect.getMetadata("keypath", target);
523
+ const keyPathMetadata = Reflect.getMetadata('keypath', target);
245
524
  if (!keyPathMetadata) {
246
525
  throw new Error(`No keypath field defined for the class ${target.name}.`);
247
526
  }
248
- const individualKeypaths = Reflect.getMetadata("individual_keypaths", target) || [];
527
+ const individualKeypaths = Reflect.getMetadata('individual_keypaths', target) || [];
249
528
  if (individualKeypaths.length > 1) {
250
529
  throw new Error(`Only one keypath field can be defined for the class ${target.name}.`);
251
530
  }
252
531
  const version = options.version || 1;
253
- Reflect.defineMetadata("dataclass", true, target);
254
- Reflect.defineMetadata("version", version, target);
532
+ Reflect.defineMetadata('dataclass', true, target);
533
+ Reflect.defineMetadata('version', version, target);
255
534
  };
256
535
  }
257
- exports.DataClass = DataClass;
258
536
  class Database {
259
537
  constructor(dbName, classes) {
260
538
  this.db = null;
@@ -262,8 +540,8 @@ class Database {
262
540
  this.retentionTimer = null;
263
541
  this.retentionCleanupRunning = false;
264
542
  this.dbName = dbName;
265
- if (!classes.every(cls => Reflect.getMetadata("dataclass", cls))) {
266
- throw new Error("All classes should be decorated with @DataClass.");
543
+ if (!classes.every((cls) => Reflect.getMetadata('dataclass', cls))) {
544
+ throw new Error('All classes should be decorated with @DataClass.');
267
545
  }
268
546
  this.classes = classes;
269
547
  this.dbVersion = this.calculateDatabaseVersion();
@@ -276,13 +554,13 @@ class Database {
276
554
  return {
277
555
  className: cls.name,
278
556
  storeName: cls.name.toLowerCase(),
279
- policy
557
+ policy,
280
558
  };
281
559
  })
282
560
  .filter((policy) => policy !== null);
283
561
  }
284
562
  calculateDatabaseVersion() {
285
- const versions = this.classes.map(cls => Reflect.getMetadata("version", cls) || 1);
563
+ const versions = this.classes.map((cls) => Reflect.getMetadata('version', cls) || 1);
286
564
  return Math.max(...versions);
287
565
  }
288
566
  static build(dbName, classes) {
@@ -304,9 +582,9 @@ class Database {
304
582
  console.debug(`Database upgrade from version ${oldVersion} to ${newVersion}`);
305
583
  this.classes.forEach((cls) => {
306
584
  var _a;
307
- const keyPathMetadata = Reflect.getMetadata("keypath", cls);
308
- const indexFields = Reflect.getMetadata("indexes", cls) || [];
309
- const classVersion = Reflect.getMetadata("version", cls) || 1;
585
+ const keyPathMetadata = Reflect.getMetadata('keypath', cls);
586
+ const indexFields = Reflect.getMetadata('indexes', cls) || [];
587
+ const classVersion = Reflect.getMetadata('version', cls) || 1;
310
588
  const storeName = cls.name.toLowerCase();
311
589
  if (classVersion > oldVersion) {
312
590
  if (!db.objectStoreNames.contains(storeName)) {
@@ -321,7 +599,9 @@ class Database {
321
599
  const store = db.createObjectStore(storeName, storeOptions);
322
600
  indexFields.forEach((indexField) => {
323
601
  var _a;
324
- const indexName = typeof indexField === 'string' ? indexField : indexField.field;
602
+ const indexName = typeof indexField === 'string'
603
+ ? indexField
604
+ : indexField.field;
325
605
  const indexOptions = typeof indexField === 'string'
326
606
  ? { unique: false }
327
607
  : ((_a = indexField.options) !== null && _a !== void 0 ? _a : { unique: false });
@@ -337,7 +617,9 @@ class Database {
337
617
  const store = transaction.objectStore(storeName);
338
618
  indexFields.forEach((indexField) => {
339
619
  var _a;
340
- const indexName = typeof indexField === 'string' ? indexField : indexField.field;
620
+ const indexName = typeof indexField === 'string'
621
+ ? indexField
622
+ : indexField.field;
341
623
  const indexOptions = typeof indexField === 'string'
342
624
  ? { unique: false }
343
625
  : ((_a = indexField.options) !== null && _a !== void 0 ? _a : { unique: false });
@@ -353,19 +635,19 @@ class Database {
353
635
  };
354
636
  request.onsuccess = () => {
355
637
  this.db = request.result;
356
- console.debug(`Database initialized (version ${this.dbVersion}) with object stores for: ${this.classes.map(cls => `${cls.name}(v${Reflect.getMetadata("version", cls) || 1})`).join(", ")}`);
638
+ console.debug(`Database initialized (version ${this.dbVersion}) with object stores for: ${this.classes.map((cls) => `${cls.name}(v${Reflect.getMetadata('version', cls) || 1})`).join(', ')}`);
357
639
  this.startRetentionCleanup();
358
640
  resolve();
359
641
  };
360
642
  request.onerror = () => {
361
- console.error("Error initializing database:", request.error);
643
+ console.error('Error initializing database:', request.error);
362
644
  reject(request.error);
363
645
  };
364
646
  });
365
647
  });
366
648
  }
367
649
  generateEntityRepositories() {
368
- this.classes.forEach(cls => {
650
+ this.classes.forEach((cls) => {
369
651
  const entityName = cls.name;
370
652
  const repository = this.createEntityRepository(cls);
371
653
  this.entityRepositories.set(entityName, repository);
@@ -373,7 +655,7 @@ class Database {
373
655
  value: repository,
374
656
  writable: false,
375
657
  enumerable: true,
376
- configurable: false
658
+ configurable: false,
377
659
  });
378
660
  });
379
661
  }
@@ -392,7 +674,7 @@ class Database {
392
674
  return Math.abs(a);
393
675
  };
394
676
  const seconds = this.retentionPolicies.map(({ policy }) => policy.seconds);
395
- return seconds.reduce((accumulator, value) => gcd(accumulator, value)) * 1000;
677
+ return (seconds.reduce((accumulator, value) => gcd(accumulator, value)) * 1000);
396
678
  }
397
679
  startRetentionCleanup() {
398
680
  const cleanupIntervalMs = this.calculateRetentionCleanupIntervalMs();
@@ -407,7 +689,9 @@ class Database {
407
689
  }
408
690
  runRetentionCleanup() {
409
691
  return __awaiter(this, void 0, void 0, function* () {
410
- if (!this.db || this.retentionCleanupRunning || !this.retentionPolicies.length) {
692
+ if (!this.db ||
693
+ this.retentionCleanupRunning ||
694
+ !this.retentionPolicies.length) {
411
695
  return;
412
696
  }
413
697
  this.retentionCleanupRunning = true;
@@ -431,7 +715,7 @@ class Database {
431
715
  try {
432
716
  const transaction = this.db.transaction(storeName, 'readwrite');
433
717
  const store = transaction.objectStore(storeName);
434
- const cutoff = Date.now() - (policy.seconds * 1000);
718
+ const cutoff = Date.now() - policy.seconds * 1000;
435
719
  const request = store.openCursor();
436
720
  request.onsuccess = () => {
437
721
  const cursor = request.result;
@@ -447,25 +731,35 @@ class Database {
447
731
  console.debug(`Retention cleanup removed expired record from ${className}`);
448
732
  cursor.continue();
449
733
  };
450
- deleteRequest.onerror = () => { var _a; return reject((_a = deleteRequest.error) !== null && _a !== void 0 ? _a : new Error(`Retention cleanup delete failed for ${className}`)); };
734
+ deleteRequest.onerror = () => {
735
+ var _a;
736
+ return reject((_a = deleteRequest.error) !== null && _a !== void 0 ? _a : new Error(`Retention cleanup delete failed for ${className}`));
737
+ };
451
738
  return;
452
739
  }
453
740
  cursor.continue();
454
741
  };
455
742
  transaction.oncomplete = () => resolve();
456
- transaction.onerror = () => { var _a; return reject((_a = transaction.error) !== null && _a !== void 0 ? _a : new Error(`Retention cleanup failed for ${className}`)); };
457
- transaction.onabort = () => { var _a; return reject((_a = transaction.error) !== null && _a !== void 0 ? _a : new Error(`Retention cleanup aborted for ${className}`)); };
743
+ transaction.onerror = () => {
744
+ var _a;
745
+ return reject((_a = transaction.error) !== null && _a !== void 0 ? _a : new Error(`Retention cleanup failed for ${className}`));
746
+ };
747
+ transaction.onabort = () => {
748
+ var _a;
749
+ return reject((_a = transaction.error) !== null && _a !== void 0 ? _a : new Error(`Retention cleanup aborted for ${className}`));
750
+ };
458
751
  }
459
752
  catch (error) {
460
753
  reject(error);
461
754
  }
462
755
  });
463
756
  }
464
- createEntityRepository(cls) {
757
+ createEntityRepository(cls, transaction) {
465
758
  const self = this;
466
759
  const creationTimestampField = INTERNAL_CREATED_AT_FIELD;
467
760
  const updateTimestampField = INTERNAL_UPDATED_AT_FIELD;
468
- const validators = (Reflect.getMetadata('validators', cls) || []);
761
+ const validators = (Reflect.getMetadata('validators', cls) ||
762
+ []);
469
763
  const validateItem = (item) => {
470
764
  const failures = [];
471
765
  validators.forEach((rule) => {
@@ -487,7 +781,7 @@ class Database {
487
781
  };
488
782
  const generateKey = (item) => {
489
783
  var _a;
490
- const keyPathMetadata = Reflect.getMetadata("keypath", cls);
784
+ const keyPathMetadata = Reflect.getMetadata('keypath', cls);
491
785
  if (!((_a = keyPathMetadata === null || keyPathMetadata === void 0 ? void 0 : keyPathMetadata.options) === null || _a === void 0 ? void 0 : _a.generator))
492
786
  return undefined;
493
787
  const generator = keyPathMetadata.options.generator;
@@ -507,8 +801,11 @@ class Database {
507
801
  };
508
802
  const applyTimestampFields = (item, existingItem) => {
509
803
  const now = Date.now();
510
- const existingCreationValue = existingItem ? existingItem[creationTimestampField] : undefined;
511
- item[creationTimestampField] = existingCreationValue !== undefined ? existingCreationValue : now;
804
+ const existingCreationValue = existingItem
805
+ ? existingItem[creationTimestampField]
806
+ : undefined;
807
+ item[creationTimestampField] =
808
+ existingCreationValue !== undefined ? existingCreationValue : now;
512
809
  item[updateTimestampField] = now;
513
810
  };
514
811
  const readExistingItem = (store, key) => {
@@ -546,19 +843,19 @@ class Database {
546
843
  });
547
844
  };
548
845
  const extractKey = (item) => {
549
- const keyPathMetadata = Reflect.getMetadata("keypath", cls);
846
+ const keyPathMetadata = Reflect.getMetadata('keypath', cls);
550
847
  if (!keyPathMetadata)
551
848
  return undefined;
552
849
  const fields = keyPathMetadata.fields;
553
850
  if (Array.isArray(fields)) {
554
- return fields.map(field => item[field]);
851
+ return fields.map((field) => item[field]);
555
852
  }
556
853
  else {
557
854
  return item[fields];
558
855
  }
559
856
  };
560
857
  const setKey = (item, key) => {
561
- const keyPathMetadata = Reflect.getMetadata("keypath", cls);
858
+ const keyPathMetadata = Reflect.getMetadata('keypath', cls);
562
859
  if (!keyPathMetadata)
563
860
  return;
564
861
  const fields = keyPathMetadata.fields;
@@ -571,14 +868,17 @@ class Database {
571
868
  if (!self.db)
572
869
  throw new Error('Database not initialized.');
573
870
  const storeName = cls.name.toLowerCase();
574
- return new QueryBuilder(self.db, storeName);
871
+ return new QueryBuilder(self.db, storeName, transaction);
575
872
  },
576
873
  create: (item) => __awaiter(this, void 0, void 0, function* () {
577
874
  var _a;
578
- const keyPathMetadata = Reflect.getMetadata("keypath", cls);
579
- if (((_a = keyPathMetadata === null || keyPathMetadata === void 0 ? void 0 : keyPathMetadata.options) === null || _a === void 0 ? void 0 : _a.generator) && !keyPathMetadata.options.autoIncrement) {
875
+ const keyPathMetadata = Reflect.getMetadata('keypath', cls);
876
+ if (((_a = keyPathMetadata === null || keyPathMetadata === void 0 ? void 0 : keyPathMetadata.options) === null || _a === void 0 ? void 0 : _a.generator) &&
877
+ !keyPathMetadata.options.autoIncrement) {
580
878
  const currentKey = extractKey(item);
581
- if (currentKey === undefined || currentKey === null || currentKey === '') {
879
+ if (currentKey === undefined ||
880
+ currentKey === null ||
881
+ currentKey === '') {
582
882
  const generatedKey = generateKey(item);
583
883
  if (generatedKey !== undefined) {
584
884
  setKey(item, generatedKey);
@@ -591,10 +891,10 @@ class Database {
591
891
  return createStoredItem(store, item).then(() => {
592
892
  console.debug(`Item added to ${cls.name}:`, item);
593
893
  });
594
- });
894
+ }, transaction);
595
895
  }),
596
896
  createMany: (items) => __awaiter(this, void 0, void 0, function* () {
597
- const repository = this.createEntityRepository(cls);
897
+ const repository = this.createEntityRepository(cls, transaction);
598
898
  for (const item of items) {
599
899
  yield repository.create(item);
600
900
  }
@@ -609,7 +909,7 @@ class Database {
609
909
  };
610
910
  request.onerror = () => reject(request.error);
611
911
  });
612
- });
912
+ }, transaction);
613
913
  }),
614
914
  update: (item) => __awaiter(this, void 0, void 0, function* () {
615
915
  validateItem(item);
@@ -617,10 +917,10 @@ class Database {
617
917
  return updateStoredItem(store, item).then(() => {
618
918
  console.debug(`Item updated in ${cls.name}:`, item);
619
919
  });
620
- });
920
+ }, transaction);
621
921
  }),
622
922
  updateMany: (items) => __awaiter(this, void 0, void 0, function* () {
623
- const repository = this.createEntityRepository(cls);
923
+ const repository = this.createEntityRepository(cls, transaction);
624
924
  for (const item of items) {
625
925
  yield repository.update(item);
626
926
  }
@@ -630,19 +930,19 @@ class Database {
630
930
  return deleteStoredItem(store, key).then(() => {
631
931
  console.debug(`Item deleted from ${cls.name}:`, key);
632
932
  });
633
- });
933
+ }, transaction);
634
934
  }),
635
935
  deleteMany: (keys) => __awaiter(this, void 0, void 0, function* () {
636
- const repository = this.createEntityRepository(cls);
936
+ const repository = this.createEntityRepository(cls, transaction);
637
937
  for (const key of keys) {
638
938
  yield repository.delete(key);
639
939
  }
640
940
  }),
641
941
  deleteWhere: (predicate) => __awaiter(this, void 0, void 0, function* () {
642
- var _b;
643
- const repository = this.createEntityRepository(cls);
942
+ var _a;
943
+ const repository = this.createEntityRepository(cls, transaction);
644
944
  const query = repository.query();
645
- const resolvedQuery = (_b = predicate(query)) !== null && _b !== void 0 ? _b : query;
945
+ const resolvedQuery = (_a = predicate(query)) !== null && _a !== void 0 ? _a : query;
646
946
  const matches = yield resolvedQuery.execute();
647
947
  const keys = matches
648
948
  .map((item) => extractKey(item))
@@ -659,7 +959,7 @@ class Database {
659
959
  };
660
960
  request.onerror = () => reject(request.error);
661
961
  });
662
- });
962
+ }, transaction);
663
963
  }),
664
964
  listPaginated: (page, pageSize) => __awaiter(this, void 0, void 0, function* () {
665
965
  return this.performOperation(cls.name, 'readonly', (store) => {
@@ -673,7 +973,7 @@ class Database {
673
973
  };
674
974
  request.onerror = () => reject(request.error);
675
975
  });
676
- });
976
+ }, transaction);
677
977
  }),
678
978
  findByIndex: (indexName, value) => __awaiter(this, void 0, void 0, function* () {
679
979
  return this.performOperation(cls.name, 'readonly', (store) => {
@@ -689,7 +989,7 @@ class Database {
689
989
  };
690
990
  request.onerror = () => reject(request.error);
691
991
  });
692
- });
992
+ }, transaction);
693
993
  }),
694
994
  findOneByIndex: (indexName, value) => __awaiter(this, void 0, void 0, function* () {
695
995
  return this.performOperation(cls.name, 'readonly', (store) => {
@@ -705,7 +1005,7 @@ class Database {
705
1005
  };
706
1006
  request.onerror = () => reject(request.error);
707
1007
  });
708
- });
1008
+ }, transaction);
709
1009
  }),
710
1010
  count: () => __awaiter(this, void 0, void 0, function* () {
711
1011
  return this.performOperation(cls.name, 'readonly', (store) => {
@@ -717,7 +1017,7 @@ class Database {
717
1017
  };
718
1018
  request.onerror = () => reject(request.error);
719
1019
  });
720
- });
1020
+ }, transaction);
721
1021
  }),
722
1022
  exists: (key) => __awaiter(this, void 0, void 0, function* () {
723
1023
  return this.performOperation(cls.name, 'readonly', (store) => {
@@ -730,7 +1030,7 @@ class Database {
730
1030
  };
731
1031
  request.onerror = () => reject(request.error);
732
1032
  });
733
- });
1033
+ }, transaction);
734
1034
  }),
735
1035
  clear: () => __awaiter(this, void 0, void 0, function* () {
736
1036
  return this.performOperation(cls.name, 'readwrite', (store) => {
@@ -742,21 +1042,97 @@ class Database {
742
1042
  };
743
1043
  request.onerror = () => reject(request.error);
744
1044
  });
745
- });
746
- })
1045
+ }, transaction);
1046
+ }),
747
1047
  };
748
1048
  }
749
- performOperation(className, mode, operation) {
1049
+ performOperation(className, mode, operation, transaction) {
750
1050
  return __awaiter(this, void 0, void 0, function* () {
751
- if (!this.db) {
752
- throw new Error("Database not initialized.");
1051
+ if (!this.db && !transaction) {
1052
+ throw new Error('Database not initialized.');
753
1053
  }
754
1054
  const storeName = className.toLowerCase();
755
- const transaction = this.db.transaction(storeName, mode);
756
- const store = transaction.objectStore(storeName);
1055
+ const activeTransaction = transaction !== null && transaction !== void 0 ? transaction : this.db.transaction(storeName, mode);
1056
+ const store = activeTransaction.objectStore(storeName);
757
1057
  return operation(store);
758
1058
  });
759
1059
  }
1060
+ createTransactionHandle(entityNames, mode) {
1061
+ if (!this.db) {
1062
+ throw new Error('Database not initialized.');
1063
+ }
1064
+ const uniqueEntityNames = [...new Set(entityNames)];
1065
+ const storeNames = uniqueEntityNames.map((entityName) => {
1066
+ const entityClass = this.classes.find((cls) => cls.name === entityName);
1067
+ if (!entityClass) {
1068
+ throw new Error(`Entity '${entityName}' is not registered in ${this.dbName}.`);
1069
+ }
1070
+ return entityClass.name.toLowerCase();
1071
+ });
1072
+ const nativeTransaction = this.db.transaction(storeNames, mode);
1073
+ let rollbackRequested = false;
1074
+ const completion = new Promise((resolve, reject) => {
1075
+ nativeTransaction.oncomplete = () => resolve();
1076
+ nativeTransaction.onabort = () => {
1077
+ var _a;
1078
+ if (rollbackRequested) {
1079
+ resolve();
1080
+ return;
1081
+ }
1082
+ reject((_a = nativeTransaction.error) !== null && _a !== void 0 ? _a : new Error('Transaction aborted.'));
1083
+ };
1084
+ nativeTransaction.onerror = () => {
1085
+ var _a;
1086
+ if (rollbackRequested) {
1087
+ resolve();
1088
+ return;
1089
+ }
1090
+ reject((_a = nativeTransaction.error) !== null && _a !== void 0 ? _a : new Error('Transaction failed.'));
1091
+ };
1092
+ });
1093
+ const handle = {};
1094
+ uniqueEntityNames.forEach((entityName) => {
1095
+ const entityClass = this.classes.find((cls) => cls.name === entityName);
1096
+ if (entityClass) {
1097
+ handle[entityName] = this.createEntityRepository(entityClass, nativeTransaction);
1098
+ }
1099
+ });
1100
+ handle.commit = () => __awaiter(this, void 0, void 0, function* () {
1101
+ if (typeof nativeTransaction.commit === 'function') {
1102
+ nativeTransaction.commit();
1103
+ }
1104
+ yield completion;
1105
+ });
1106
+ handle.rollback = () => __awaiter(this, void 0, void 0, function* () {
1107
+ rollbackRequested = true;
1108
+ try {
1109
+ nativeTransaction.abort();
1110
+ }
1111
+ catch (_a) {
1112
+ }
1113
+ yield completion.catch(() => undefined);
1114
+ });
1115
+ return handle;
1116
+ }
1117
+ beginTransaction(entityNames_1) {
1118
+ return __awaiter(this, arguments, void 0, function* (entityNames, mode = 'readwrite') {
1119
+ return this.createTransactionHandle(entityNames, mode);
1120
+ });
1121
+ }
1122
+ transaction(callback) {
1123
+ return __awaiter(this, void 0, void 0, function* () {
1124
+ const tx = this.createTransactionHandle(this.classes.map((cls) => cls.name), 'readwrite');
1125
+ try {
1126
+ const result = yield callback(tx);
1127
+ yield tx.commit();
1128
+ return result;
1129
+ }
1130
+ catch (error) {
1131
+ yield tx.rollback();
1132
+ throw error;
1133
+ }
1134
+ });
1135
+ }
760
1136
  close() {
761
1137
  if (this.retentionTimer) {
762
1138
  clearInterval(this.retentionTimer);
@@ -775,15 +1151,15 @@ class Database {
775
1151
  }
776
1152
  getEntityVersions() {
777
1153
  const versions = new Map();
778
- this.classes.forEach(cls => {
779
- const version = Reflect.getMetadata("version", cls) || 1;
1154
+ this.classes.forEach((cls) => {
1155
+ const version = Reflect.getMetadata('version', cls) || 1;
780
1156
  versions.set(cls.name, version);
781
1157
  });
782
1158
  return versions;
783
1159
  }
784
1160
  getEntityVersion(entityName) {
785
- const cls = this.classes.find(c => c.name === entityName);
786
- return cls ? (Reflect.getMetadata("version", cls) || 1) : undefined;
1161
+ const cls = this.classes.find((c) => c.name === entityName);
1162
+ return cls ? Reflect.getMetadata('version', cls) || 1 : undefined;
787
1163
  }
788
1164
  }
789
1165
  exports.Database = Database;