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