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.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.
|
|
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.
|
|
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
|
-
|
|
22
|
-
this.
|
|
129
|
+
or() {
|
|
130
|
+
this.pendingConnector = 'or';
|
|
23
131
|
return this;
|
|
24
132
|
}
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.
|
|
62
|
-
|
|
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,92 +229,226 @@ class QueryBuilder {
|
|
|
83
229
|
this.rangeEnd = end;
|
|
84
230
|
return this;
|
|
85
231
|
}
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
let 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
|
}
|
|
445
|
+
const INTERNAL_CREATED_AT_FIELD = '__idb_createdAt';
|
|
446
|
+
const INTERNAL_UPDATED_AT_FIELD = '__idb_updatedAt';
|
|
167
447
|
class KeyGenerators {
|
|
168
448
|
static uuid() {
|
|
169
449
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
170
|
-
const r = Math.random() * 16 | 0;
|
|
171
|
-
const v = c === 'x' ? r : (r & 0x3 | 0x8
|
|
450
|
+
const r = (Math.random() * 16) | 0;
|
|
451
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
172
452
|
return v.toString(16);
|
|
173
453
|
});
|
|
174
454
|
}
|
|
@@ -183,64 +463,104 @@ exports.KeyGenerators = KeyGenerators;
|
|
|
183
463
|
function KeyPath(options) {
|
|
184
464
|
return (target, propertyKey) => {
|
|
185
465
|
const constructor = target.constructor;
|
|
186
|
-
const existingKeypaths = Reflect.getMetadata(
|
|
466
|
+
const existingKeypaths = Reflect.getMetadata('individual_keypaths', constructor) || [];
|
|
187
467
|
existingKeypaths.push(propertyKey);
|
|
188
|
-
Reflect.defineMetadata(
|
|
468
|
+
Reflect.defineMetadata('individual_keypaths', existingKeypaths, constructor);
|
|
189
469
|
const metadata = {
|
|
190
470
|
fields: propertyKey,
|
|
191
|
-
options: options
|
|
471
|
+
options: options,
|
|
192
472
|
};
|
|
193
|
-
Reflect.defineMetadata(
|
|
473
|
+
Reflect.defineMetadata('keypath', metadata, constructor);
|
|
194
474
|
};
|
|
195
475
|
}
|
|
196
|
-
exports.KeyPath = KeyPath;
|
|
197
476
|
function CompositeKeyPath(fields, options) {
|
|
198
477
|
return (target) => {
|
|
199
478
|
const metadata = {
|
|
200
479
|
fields: fields,
|
|
201
|
-
options: options
|
|
480
|
+
options: options,
|
|
202
481
|
};
|
|
203
|
-
Reflect.defineMetadata(
|
|
482
|
+
Reflect.defineMetadata('keypath', metadata, target);
|
|
204
483
|
};
|
|
205
484
|
}
|
|
206
|
-
|
|
207
|
-
function Index() {
|
|
485
|
+
function Index(options) {
|
|
208
486
|
return (target, propertyKey) => {
|
|
209
487
|
const constructor = target.constructor;
|
|
210
|
-
const existing = Reflect.getMetadata(
|
|
211
|
-
|
|
488
|
+
const existing = Reflect.getMetadata('indexes', constructor) || [];
|
|
489
|
+
const nextIndexes = [
|
|
490
|
+
...existing,
|
|
491
|
+
{ field: propertyKey, options },
|
|
492
|
+
];
|
|
493
|
+
Reflect.defineMetadata('indexes', nextIndexes, constructor);
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function Validate(predicate, message) {
|
|
497
|
+
return (target, propertyKey) => {
|
|
498
|
+
const constructor = target.constructor;
|
|
499
|
+
const existing = Reflect.getMetadata('validators', constructor) || [];
|
|
500
|
+
const nextRules = [
|
|
501
|
+
...existing,
|
|
502
|
+
{ field: propertyKey, predicate, message },
|
|
503
|
+
];
|
|
504
|
+
Reflect.defineMetadata('validators', nextRules, constructor);
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function RetentionPolicy(options) {
|
|
508
|
+
return (target) => {
|
|
509
|
+
var _a, _b;
|
|
510
|
+
if (!Number.isInteger(options.seconds) || options.seconds <= 0) {
|
|
511
|
+
throw new Error('RetentionPolicy.seconds must be a positive integer.');
|
|
512
|
+
}
|
|
513
|
+
const metadata = {
|
|
514
|
+
seconds: options.seconds,
|
|
515
|
+
enabled: (_a = options.enabled) !== null && _a !== void 0 ? _a : true,
|
|
516
|
+
field: (_b = options.field) !== null && _b !== void 0 ? _b : INTERNAL_CREATED_AT_FIELD,
|
|
517
|
+
};
|
|
518
|
+
Reflect.defineMetadata('retention_policy', metadata, target);
|
|
212
519
|
};
|
|
213
520
|
}
|
|
214
|
-
exports.Index = Index;
|
|
215
521
|
function DataClass(options = {}) {
|
|
216
522
|
return (target) => {
|
|
217
|
-
const keyPathMetadata = Reflect.getMetadata(
|
|
523
|
+
const keyPathMetadata = Reflect.getMetadata('keypath', target);
|
|
218
524
|
if (!keyPathMetadata) {
|
|
219
525
|
throw new Error(`No keypath field defined for the class ${target.name}.`);
|
|
220
526
|
}
|
|
221
|
-
const individualKeypaths = Reflect.getMetadata(
|
|
527
|
+
const individualKeypaths = Reflect.getMetadata('individual_keypaths', target) || [];
|
|
222
528
|
if (individualKeypaths.length > 1) {
|
|
223
529
|
throw new Error(`Only one keypath field can be defined for the class ${target.name}.`);
|
|
224
530
|
}
|
|
225
531
|
const version = options.version || 1;
|
|
226
|
-
Reflect.defineMetadata(
|
|
227
|
-
Reflect.defineMetadata(
|
|
532
|
+
Reflect.defineMetadata('dataclass', true, target);
|
|
533
|
+
Reflect.defineMetadata('version', version, target);
|
|
228
534
|
};
|
|
229
535
|
}
|
|
230
|
-
exports.DataClass = DataClass;
|
|
231
536
|
class Database {
|
|
232
537
|
constructor(dbName, classes) {
|
|
233
538
|
this.db = null;
|
|
234
539
|
this.entityRepositories = new Map();
|
|
540
|
+
this.retentionTimer = null;
|
|
541
|
+
this.retentionCleanupRunning = false;
|
|
235
542
|
this.dbName = dbName;
|
|
236
|
-
if (!classes.every(cls => Reflect.getMetadata(
|
|
237
|
-
throw new Error(
|
|
543
|
+
if (!classes.every((cls) => Reflect.getMetadata('dataclass', cls))) {
|
|
544
|
+
throw new Error('All classes should be decorated with @DataClass.');
|
|
238
545
|
}
|
|
239
546
|
this.classes = classes;
|
|
240
547
|
this.dbVersion = this.calculateDatabaseVersion();
|
|
548
|
+
this.retentionPolicies = this.classes
|
|
549
|
+
.map((cls) => {
|
|
550
|
+
const policy = Reflect.getMetadata('retention_policy', cls);
|
|
551
|
+
if (!(policy === null || policy === void 0 ? void 0 : policy.enabled)) {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
className: cls.name,
|
|
556
|
+
storeName: cls.name.toLowerCase(),
|
|
557
|
+
policy,
|
|
558
|
+
};
|
|
559
|
+
})
|
|
560
|
+
.filter((policy) => policy !== null);
|
|
241
561
|
}
|
|
242
562
|
calculateDatabaseVersion() {
|
|
243
|
-
const versions = this.classes.map(cls => Reflect.getMetadata(
|
|
563
|
+
const versions = this.classes.map((cls) => Reflect.getMetadata('version', cls) || 1);
|
|
244
564
|
return Math.max(...versions);
|
|
245
565
|
}
|
|
246
566
|
static build(dbName, classes) {
|
|
@@ -262,9 +582,9 @@ class Database {
|
|
|
262
582
|
console.debug(`Database upgrade from version ${oldVersion} to ${newVersion}`);
|
|
263
583
|
this.classes.forEach((cls) => {
|
|
264
584
|
var _a;
|
|
265
|
-
const keyPathMetadata = Reflect.getMetadata(
|
|
266
|
-
const indexFields = Reflect.getMetadata(
|
|
267
|
-
const classVersion = Reflect.getMetadata(
|
|
585
|
+
const keyPathMetadata = Reflect.getMetadata('keypath', cls);
|
|
586
|
+
const indexFields = Reflect.getMetadata('indexes', cls) || [];
|
|
587
|
+
const classVersion = Reflect.getMetadata('version', cls) || 1;
|
|
268
588
|
const storeName = cls.name.toLowerCase();
|
|
269
589
|
if (classVersion > oldVersion) {
|
|
270
590
|
if (!db.objectStoreNames.contains(storeName)) {
|
|
@@ -278,8 +598,15 @@ class Database {
|
|
|
278
598
|
}
|
|
279
599
|
const store = db.createObjectStore(storeName, storeOptions);
|
|
280
600
|
indexFields.forEach((indexField) => {
|
|
281
|
-
|
|
282
|
-
|
|
601
|
+
var _a;
|
|
602
|
+
const indexName = typeof indexField === 'string'
|
|
603
|
+
? indexField
|
|
604
|
+
: indexField.field;
|
|
605
|
+
const indexOptions = typeof indexField === 'string'
|
|
606
|
+
? { unique: false }
|
|
607
|
+
: ((_a = indexField.options) !== null && _a !== void 0 ? _a : { unique: false });
|
|
608
|
+
if (!store.indexNames.contains(indexName)) {
|
|
609
|
+
store.createIndex(indexName, indexName, indexOptions);
|
|
283
610
|
}
|
|
284
611
|
});
|
|
285
612
|
}
|
|
@@ -289,9 +616,16 @@ class Database {
|
|
|
289
616
|
if (transaction) {
|
|
290
617
|
const store = transaction.objectStore(storeName);
|
|
291
618
|
indexFields.forEach((indexField) => {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
619
|
+
var _a;
|
|
620
|
+
const indexName = typeof indexField === 'string'
|
|
621
|
+
? indexField
|
|
622
|
+
: indexField.field;
|
|
623
|
+
const indexOptions = typeof indexField === 'string'
|
|
624
|
+
? { unique: false }
|
|
625
|
+
: ((_a = indexField.options) !== null && _a !== void 0 ? _a : { unique: false });
|
|
626
|
+
if (!store.indexNames.contains(indexName)) {
|
|
627
|
+
console.debug(`Adding index: ${indexName} to ${storeName}`);
|
|
628
|
+
store.createIndex(indexName, indexName, indexOptions);
|
|
295
629
|
}
|
|
296
630
|
});
|
|
297
631
|
}
|
|
@@ -301,18 +635,19 @@ class Database {
|
|
|
301
635
|
};
|
|
302
636
|
request.onsuccess = () => {
|
|
303
637
|
this.db = request.result;
|
|
304
|
-
console.debug(`Database initialized (version ${this.dbVersion}) with object stores for: ${this.classes.map(cls => `${cls.name}(v${Reflect.getMetadata(
|
|
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(', ')}`);
|
|
639
|
+
this.startRetentionCleanup();
|
|
305
640
|
resolve();
|
|
306
641
|
};
|
|
307
642
|
request.onerror = () => {
|
|
308
|
-
console.error(
|
|
643
|
+
console.error('Error initializing database:', request.error);
|
|
309
644
|
reject(request.error);
|
|
310
645
|
};
|
|
311
646
|
});
|
|
312
647
|
});
|
|
313
648
|
}
|
|
314
649
|
generateEntityRepositories() {
|
|
315
|
-
this.classes.forEach(cls => {
|
|
650
|
+
this.classes.forEach((cls) => {
|
|
316
651
|
const entityName = cls.name;
|
|
317
652
|
const repository = this.createEntityRepository(cls);
|
|
318
653
|
this.entityRepositories.set(entityName, repository);
|
|
@@ -320,15 +655,133 @@ class Database {
|
|
|
320
655
|
value: repository,
|
|
321
656
|
writable: false,
|
|
322
657
|
enumerable: true,
|
|
323
|
-
configurable: false
|
|
658
|
+
configurable: false,
|
|
324
659
|
});
|
|
325
660
|
});
|
|
326
661
|
}
|
|
327
|
-
|
|
662
|
+
calculateRetentionCleanupIntervalMs() {
|
|
663
|
+
if (!this.retentionPolicies.length) {
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
const gcd = (left, right) => {
|
|
667
|
+
let a = left;
|
|
668
|
+
let b = right;
|
|
669
|
+
while (b !== 0) {
|
|
670
|
+
const remainder = a % b;
|
|
671
|
+
a = b;
|
|
672
|
+
b = remainder;
|
|
673
|
+
}
|
|
674
|
+
return Math.abs(a);
|
|
675
|
+
};
|
|
676
|
+
const seconds = this.retentionPolicies.map(({ policy }) => policy.seconds);
|
|
677
|
+
return (seconds.reduce((accumulator, value) => gcd(accumulator, value)) * 1000);
|
|
678
|
+
}
|
|
679
|
+
startRetentionCleanup() {
|
|
680
|
+
const cleanupIntervalMs = this.calculateRetentionCleanupIntervalMs();
|
|
681
|
+
if (!cleanupIntervalMs || !this.db || this.retentionTimer) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
console.debug(`Retention cleanup enabled for ${this.retentionPolicies.length} entities every ${cleanupIntervalMs}ms`);
|
|
685
|
+
void this.runRetentionCleanup();
|
|
686
|
+
this.retentionTimer = setInterval(() => {
|
|
687
|
+
void this.runRetentionCleanup();
|
|
688
|
+
}, cleanupIntervalMs);
|
|
689
|
+
}
|
|
690
|
+
runRetentionCleanup() {
|
|
691
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
692
|
+
if (!this.db ||
|
|
693
|
+
this.retentionCleanupRunning ||
|
|
694
|
+
!this.retentionPolicies.length) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
this.retentionCleanupRunning = true;
|
|
698
|
+
try {
|
|
699
|
+
console.debug('Retention cleanup tick started');
|
|
700
|
+
for (const { storeName, className, policy } of this.retentionPolicies) {
|
|
701
|
+
yield this.cleanupExpiredRecords(storeName, className, policy);
|
|
702
|
+
}
|
|
703
|
+
console.debug('Retention cleanup tick finished');
|
|
704
|
+
}
|
|
705
|
+
finally {
|
|
706
|
+
this.retentionCleanupRunning = false;
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
cleanupExpiredRecords(storeName, className, policy) {
|
|
711
|
+
if (!this.db) {
|
|
712
|
+
return Promise.resolve();
|
|
713
|
+
}
|
|
714
|
+
return new Promise((resolve, reject) => {
|
|
715
|
+
try {
|
|
716
|
+
const transaction = this.db.transaction(storeName, 'readwrite');
|
|
717
|
+
const store = transaction.objectStore(storeName);
|
|
718
|
+
const cutoff = Date.now() - policy.seconds * 1000;
|
|
719
|
+
const request = store.openCursor();
|
|
720
|
+
request.onsuccess = () => {
|
|
721
|
+
const cursor = request.result;
|
|
722
|
+
if (!cursor) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const value = cursor.value;
|
|
726
|
+
const timestamp = value === null || value === void 0 ? void 0 : value[policy.field];
|
|
727
|
+
console.debug(`Retention cleanup inspecting ${className}.${policy.field}:`, timestamp, 'cutoff:', cutoff);
|
|
728
|
+
if (typeof timestamp === 'number' && timestamp <= cutoff) {
|
|
729
|
+
const deleteRequest = cursor.delete();
|
|
730
|
+
deleteRequest.onsuccess = () => {
|
|
731
|
+
console.debug(`Retention cleanup removed expired record from ${className}`);
|
|
732
|
+
cursor.continue();
|
|
733
|
+
};
|
|
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
|
+
};
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
cursor.continue();
|
|
741
|
+
};
|
|
742
|
+
transaction.oncomplete = () => resolve();
|
|
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
|
+
};
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
reject(error);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
createEntityRepository(cls, transaction) {
|
|
328
758
|
const self = this;
|
|
759
|
+
const creationTimestampField = INTERNAL_CREATED_AT_FIELD;
|
|
760
|
+
const updateTimestampField = INTERNAL_UPDATED_AT_FIELD;
|
|
761
|
+
const validators = (Reflect.getMetadata('validators', cls) ||
|
|
762
|
+
[]);
|
|
763
|
+
const validateItem = (item) => {
|
|
764
|
+
const failures = [];
|
|
765
|
+
validators.forEach((rule) => {
|
|
766
|
+
const value = item[rule.field];
|
|
767
|
+
let valid = false;
|
|
768
|
+
try {
|
|
769
|
+
valid = rule.predicate(value, item);
|
|
770
|
+
}
|
|
771
|
+
catch (_a) {
|
|
772
|
+
valid = false;
|
|
773
|
+
}
|
|
774
|
+
if (!valid) {
|
|
775
|
+
failures.push(`${rule.field}: ${rule.message}`);
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
if (failures.length) {
|
|
779
|
+
throw new Error(`Validation failed for ${cls.name}: ${failures.join('; ')}`);
|
|
780
|
+
}
|
|
781
|
+
};
|
|
329
782
|
const generateKey = (item) => {
|
|
330
783
|
var _a;
|
|
331
|
-
const keyPathMetadata = Reflect.getMetadata(
|
|
784
|
+
const keyPathMetadata = Reflect.getMetadata('keypath', cls);
|
|
332
785
|
if (!((_a = keyPathMetadata === null || keyPathMetadata === void 0 ? void 0 : keyPathMetadata.options) === null || _a === void 0 ? void 0 : _a.generator))
|
|
333
786
|
return undefined;
|
|
334
787
|
const generator = keyPathMetadata.options.generator;
|
|
@@ -346,20 +799,63 @@ class Database {
|
|
|
346
799
|
return undefined;
|
|
347
800
|
}
|
|
348
801
|
};
|
|
802
|
+
const applyTimestampFields = (item, existingItem) => {
|
|
803
|
+
const now = Date.now();
|
|
804
|
+
const existingCreationValue = existingItem
|
|
805
|
+
? existingItem[creationTimestampField]
|
|
806
|
+
: undefined;
|
|
807
|
+
item[creationTimestampField] =
|
|
808
|
+
existingCreationValue !== undefined ? existingCreationValue : now;
|
|
809
|
+
item[updateTimestampField] = now;
|
|
810
|
+
};
|
|
811
|
+
const readExistingItem = (store, key) => {
|
|
812
|
+
return new Promise((resolve, reject) => {
|
|
813
|
+
const request = store.get(key);
|
|
814
|
+
request.onsuccess = () => resolve(request.result);
|
|
815
|
+
request.onerror = () => reject(request.error);
|
|
816
|
+
});
|
|
817
|
+
};
|
|
818
|
+
const createStoredItem = (store, item) => {
|
|
819
|
+
return new Promise((resolve, reject) => {
|
|
820
|
+
const request = store.add(item);
|
|
821
|
+
request.onsuccess = () => resolve();
|
|
822
|
+
request.onerror = () => reject(request.error);
|
|
823
|
+
});
|
|
824
|
+
};
|
|
825
|
+
const updateStoredItem = (store, item) => __awaiter(this, void 0, void 0, function* () {
|
|
826
|
+
const key = extractKey(item);
|
|
827
|
+
let existingItem;
|
|
828
|
+
if (key !== undefined && key !== null) {
|
|
829
|
+
existingItem = yield readExistingItem(store, key);
|
|
830
|
+
}
|
|
831
|
+
applyTimestampFields(item, existingItem);
|
|
832
|
+
yield new Promise((resolve, reject) => {
|
|
833
|
+
const request = store.put(item);
|
|
834
|
+
request.onsuccess = () => resolve();
|
|
835
|
+
request.onerror = () => reject(request.error);
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
const deleteStoredItem = (store, key) => {
|
|
839
|
+
return new Promise((resolve, reject) => {
|
|
840
|
+
const request = store.delete(key);
|
|
841
|
+
request.onsuccess = () => resolve();
|
|
842
|
+
request.onerror = () => reject(request.error);
|
|
843
|
+
});
|
|
844
|
+
};
|
|
349
845
|
const extractKey = (item) => {
|
|
350
|
-
const keyPathMetadata = Reflect.getMetadata(
|
|
846
|
+
const keyPathMetadata = Reflect.getMetadata('keypath', cls);
|
|
351
847
|
if (!keyPathMetadata)
|
|
352
848
|
return undefined;
|
|
353
849
|
const fields = keyPathMetadata.fields;
|
|
354
850
|
if (Array.isArray(fields)) {
|
|
355
|
-
return fields.map(field => item[field]);
|
|
851
|
+
return fields.map((field) => item[field]);
|
|
356
852
|
}
|
|
357
853
|
else {
|
|
358
854
|
return item[fields];
|
|
359
855
|
}
|
|
360
856
|
};
|
|
361
857
|
const setKey = (item, key) => {
|
|
362
|
-
const keyPathMetadata = Reflect.getMetadata(
|
|
858
|
+
const keyPathMetadata = Reflect.getMetadata('keypath', cls);
|
|
363
859
|
if (!keyPathMetadata)
|
|
364
860
|
return;
|
|
365
861
|
const fields = keyPathMetadata.fields;
|
|
@@ -372,30 +868,36 @@ class Database {
|
|
|
372
868
|
if (!self.db)
|
|
373
869
|
throw new Error('Database not initialized.');
|
|
374
870
|
const storeName = cls.name.toLowerCase();
|
|
375
|
-
return new QueryBuilder(self.db, storeName);
|
|
871
|
+
return new QueryBuilder(self.db, storeName, transaction);
|
|
376
872
|
},
|
|
377
873
|
create: (item) => __awaiter(this, void 0, void 0, function* () {
|
|
378
874
|
var _a;
|
|
379
|
-
const keyPathMetadata = Reflect.getMetadata(
|
|
380
|
-
if (((_a = keyPathMetadata === null || keyPathMetadata === void 0 ? void 0 : keyPathMetadata.options) === null || _a === void 0 ? void 0 : _a.generator) &&
|
|
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) {
|
|
381
878
|
const currentKey = extractKey(item);
|
|
382
|
-
if (currentKey === undefined ||
|
|
879
|
+
if (currentKey === undefined ||
|
|
880
|
+
currentKey === null ||
|
|
881
|
+
currentKey === '') {
|
|
383
882
|
const generatedKey = generateKey(item);
|
|
384
883
|
if (generatedKey !== undefined) {
|
|
385
884
|
setKey(item, generatedKey);
|
|
386
885
|
}
|
|
387
886
|
}
|
|
388
887
|
}
|
|
888
|
+
validateItem(item);
|
|
889
|
+
applyTimestampFields(item);
|
|
389
890
|
return this.performOperation(cls.name, 'readwrite', (store) => {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
request.onsuccess = () => {
|
|
393
|
-
console.debug(`Item added to ${cls.name}:`, item);
|
|
394
|
-
resolve();
|
|
395
|
-
};
|
|
396
|
-
request.onerror = () => reject(request.error);
|
|
891
|
+
return createStoredItem(store, item).then(() => {
|
|
892
|
+
console.debug(`Item added to ${cls.name}:`, item);
|
|
397
893
|
});
|
|
398
|
-
});
|
|
894
|
+
}, transaction);
|
|
895
|
+
}),
|
|
896
|
+
createMany: (items) => __awaiter(this, void 0, void 0, function* () {
|
|
897
|
+
const repository = this.createEntityRepository(cls, transaction);
|
|
898
|
+
for (const item of items) {
|
|
899
|
+
yield repository.create(item);
|
|
900
|
+
}
|
|
399
901
|
}),
|
|
400
902
|
read: (key) => __awaiter(this, void 0, void 0, function* () {
|
|
401
903
|
return this.performOperation(cls.name, 'readonly', (store) => {
|
|
@@ -407,31 +909,45 @@ class Database {
|
|
|
407
909
|
};
|
|
408
910
|
request.onerror = () => reject(request.error);
|
|
409
911
|
});
|
|
410
|
-
});
|
|
912
|
+
}, transaction);
|
|
411
913
|
}),
|
|
412
914
|
update: (item) => __awaiter(this, void 0, void 0, function* () {
|
|
915
|
+
validateItem(item);
|
|
413
916
|
return this.performOperation(cls.name, 'readwrite', (store) => {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
request.onsuccess = () => {
|
|
417
|
-
console.debug(`Item updated in ${cls.name}:`, item);
|
|
418
|
-
resolve();
|
|
419
|
-
};
|
|
420
|
-
request.onerror = () => reject(request.error);
|
|
917
|
+
return updateStoredItem(store, item).then(() => {
|
|
918
|
+
console.debug(`Item updated in ${cls.name}:`, item);
|
|
421
919
|
});
|
|
422
|
-
});
|
|
920
|
+
}, transaction);
|
|
921
|
+
}),
|
|
922
|
+
updateMany: (items) => __awaiter(this, void 0, void 0, function* () {
|
|
923
|
+
const repository = this.createEntityRepository(cls, transaction);
|
|
924
|
+
for (const item of items) {
|
|
925
|
+
yield repository.update(item);
|
|
926
|
+
}
|
|
423
927
|
}),
|
|
424
928
|
delete: (key) => __awaiter(this, void 0, void 0, function* () {
|
|
425
929
|
return this.performOperation(cls.name, 'readwrite', (store) => {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
request.onsuccess = () => {
|
|
429
|
-
console.debug(`Item deleted from ${cls.name}:`, key);
|
|
430
|
-
resolve();
|
|
431
|
-
};
|
|
432
|
-
request.onerror = () => reject(request.error);
|
|
930
|
+
return deleteStoredItem(store, key).then(() => {
|
|
931
|
+
console.debug(`Item deleted from ${cls.name}:`, key);
|
|
433
932
|
});
|
|
434
|
-
});
|
|
933
|
+
}, transaction);
|
|
934
|
+
}),
|
|
935
|
+
deleteMany: (keys) => __awaiter(this, void 0, void 0, function* () {
|
|
936
|
+
const repository = this.createEntityRepository(cls, transaction);
|
|
937
|
+
for (const key of keys) {
|
|
938
|
+
yield repository.delete(key);
|
|
939
|
+
}
|
|
940
|
+
}),
|
|
941
|
+
deleteWhere: (predicate) => __awaiter(this, void 0, void 0, function* () {
|
|
942
|
+
var _a;
|
|
943
|
+
const repository = this.createEntityRepository(cls, transaction);
|
|
944
|
+
const query = repository.query();
|
|
945
|
+
const resolvedQuery = (_a = predicate(query)) !== null && _a !== void 0 ? _a : query;
|
|
946
|
+
const matches = yield resolvedQuery.execute();
|
|
947
|
+
const keys = matches
|
|
948
|
+
.map((item) => extractKey(item))
|
|
949
|
+
.filter((key) => key !== undefined && key !== null);
|
|
950
|
+
yield repository.deleteMany(keys);
|
|
435
951
|
}),
|
|
436
952
|
list: () => __awaiter(this, void 0, void 0, function* () {
|
|
437
953
|
return this.performOperation(cls.name, 'readonly', (store) => {
|
|
@@ -443,7 +959,7 @@ class Database {
|
|
|
443
959
|
};
|
|
444
960
|
request.onerror = () => reject(request.error);
|
|
445
961
|
});
|
|
446
|
-
});
|
|
962
|
+
}, transaction);
|
|
447
963
|
}),
|
|
448
964
|
listPaginated: (page, pageSize) => __awaiter(this, void 0, void 0, function* () {
|
|
449
965
|
return this.performOperation(cls.name, 'readonly', (store) => {
|
|
@@ -457,7 +973,7 @@ class Database {
|
|
|
457
973
|
};
|
|
458
974
|
request.onerror = () => reject(request.error);
|
|
459
975
|
});
|
|
460
|
-
});
|
|
976
|
+
}, transaction);
|
|
461
977
|
}),
|
|
462
978
|
findByIndex: (indexName, value) => __awaiter(this, void 0, void 0, function* () {
|
|
463
979
|
return this.performOperation(cls.name, 'readonly', (store) => {
|
|
@@ -473,7 +989,7 @@ class Database {
|
|
|
473
989
|
};
|
|
474
990
|
request.onerror = () => reject(request.error);
|
|
475
991
|
});
|
|
476
|
-
});
|
|
992
|
+
}, transaction);
|
|
477
993
|
}),
|
|
478
994
|
findOneByIndex: (indexName, value) => __awaiter(this, void 0, void 0, function* () {
|
|
479
995
|
return this.performOperation(cls.name, 'readonly', (store) => {
|
|
@@ -489,7 +1005,7 @@ class Database {
|
|
|
489
1005
|
};
|
|
490
1006
|
request.onerror = () => reject(request.error);
|
|
491
1007
|
});
|
|
492
|
-
});
|
|
1008
|
+
}, transaction);
|
|
493
1009
|
}),
|
|
494
1010
|
count: () => __awaiter(this, void 0, void 0, function* () {
|
|
495
1011
|
return this.performOperation(cls.name, 'readonly', (store) => {
|
|
@@ -501,7 +1017,7 @@ class Database {
|
|
|
501
1017
|
};
|
|
502
1018
|
request.onerror = () => reject(request.error);
|
|
503
1019
|
});
|
|
504
|
-
});
|
|
1020
|
+
}, transaction);
|
|
505
1021
|
}),
|
|
506
1022
|
exists: (key) => __awaiter(this, void 0, void 0, function* () {
|
|
507
1023
|
return this.performOperation(cls.name, 'readonly', (store) => {
|
|
@@ -514,7 +1030,7 @@ class Database {
|
|
|
514
1030
|
};
|
|
515
1031
|
request.onerror = () => reject(request.error);
|
|
516
1032
|
});
|
|
517
|
-
});
|
|
1033
|
+
}, transaction);
|
|
518
1034
|
}),
|
|
519
1035
|
clear: () => __awaiter(this, void 0, void 0, function* () {
|
|
520
1036
|
return this.performOperation(cls.name, 'readwrite', (store) => {
|
|
@@ -526,21 +1042,107 @@ class Database {
|
|
|
526
1042
|
};
|
|
527
1043
|
request.onerror = () => reject(request.error);
|
|
528
1044
|
});
|
|
529
|
-
});
|
|
530
|
-
})
|
|
1045
|
+
}, transaction);
|
|
1046
|
+
}),
|
|
531
1047
|
};
|
|
532
1048
|
}
|
|
533
|
-
performOperation(className, mode, operation) {
|
|
1049
|
+
performOperation(className, mode, operation, transaction) {
|
|
534
1050
|
return __awaiter(this, void 0, void 0, function* () {
|
|
535
|
-
if (!this.db) {
|
|
536
|
-
throw new Error(
|
|
1051
|
+
if (!this.db && !transaction) {
|
|
1052
|
+
throw new Error('Database not initialized.');
|
|
537
1053
|
}
|
|
538
1054
|
const storeName = className.toLowerCase();
|
|
539
|
-
const
|
|
540
|
-
const store =
|
|
1055
|
+
const activeTransaction = transaction !== null && transaction !== void 0 ? transaction : this.db.transaction(storeName, mode);
|
|
1056
|
+
const store = activeTransaction.objectStore(storeName);
|
|
541
1057
|
return operation(store);
|
|
542
1058
|
});
|
|
543
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
|
+
}
|
|
1136
|
+
close() {
|
|
1137
|
+
if (this.retentionTimer) {
|
|
1138
|
+
clearInterval(this.retentionTimer);
|
|
1139
|
+
this.retentionTimer = null;
|
|
1140
|
+
}
|
|
1141
|
+
if (this.db) {
|
|
1142
|
+
this.db.close();
|
|
1143
|
+
this.db = null;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
544
1146
|
getAvailableEntities() {
|
|
545
1147
|
return Array.from(this.entityRepositories.keys());
|
|
546
1148
|
}
|
|
@@ -549,15 +1151,15 @@ class Database {
|
|
|
549
1151
|
}
|
|
550
1152
|
getEntityVersions() {
|
|
551
1153
|
const versions = new Map();
|
|
552
|
-
this.classes.forEach(cls => {
|
|
553
|
-
const version = Reflect.getMetadata(
|
|
1154
|
+
this.classes.forEach((cls) => {
|
|
1155
|
+
const version = Reflect.getMetadata('version', cls) || 1;
|
|
554
1156
|
versions.set(cls.name, version);
|
|
555
1157
|
});
|
|
556
1158
|
return versions;
|
|
557
1159
|
}
|
|
558
1160
|
getEntityVersion(entityName) {
|
|
559
|
-
const cls = this.classes.find(c => c.name === entityName);
|
|
560
|
-
return cls ?
|
|
1161
|
+
const cls = this.classes.find((c) => c.name === entityName);
|
|
1162
|
+
return cls ? Reflect.getMetadata('version', cls) || 1 : undefined;
|
|
561
1163
|
}
|
|
562
1164
|
}
|
|
563
1165
|
exports.Database = Database;
|