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