asljs-dali 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,7 +43,8 @@ const db =
43
43
  const notes =
44
44
  new Table<Note>(
45
45
  'notes',
46
- db);
46
+ db,
47
+ { /* options */ });
47
48
 
48
49
  await notes.add(
49
50
  { id: '1',
@@ -69,6 +70,11 @@ Versioning:
69
70
  - `IncrementTableVersionStrategy<T>`
70
71
  - `UuidTableVersionStrategy<T>`
71
72
 
73
+ Delete strategies:
74
+
75
+ - `TableDeleteStrategy<T>`
76
+ - `UuidSoftDeleteTableDeleteStrategy<T>`
77
+
72
78
  Transactions:
73
79
 
74
80
  - `TxMode`
@@ -0,0 +1,18 @@
1
+ import { type DeleteStrategy } from './delete-strategy.js';
2
+ type IndexQueryMapping = {
3
+ index: string;
4
+ key: IDBValidKey;
5
+ };
6
+ type IndexQueryMapper = (index: string, key: IDBValidKey) => IndexQueryMapping | null;
7
+ export declare class UuidSoftDeleteStrategy<T extends Record<string, any>> implements DeleteStrategy<T> {
8
+ private readonly deletedField;
9
+ private readonly mapActiveIndexQuery?;
10
+ constructor(deletedField: keyof T & string, mapActiveIndexQuery?: IndexQueryMapper | undefined);
11
+ isDeleted(record: T): boolean;
12
+ delete(record: T): T;
13
+ mapIndexQuery(index: string, key: IDBValidKey): {
14
+ index: string;
15
+ key: IDBValidKey;
16
+ } | null;
17
+ }
18
+ export {};
@@ -0,0 +1,23 @@
1
+ export class UuidSoftDeleteStrategy {
2
+ constructor(deletedField, mapActiveIndexQuery) {
3
+ this.deletedField = deletedField;
4
+ this.mapActiveIndexQuery = mapActiveIndexQuery;
5
+ }
6
+ isDeleted(record) {
7
+ const marker = record[this.deletedField];
8
+ return typeof marker === 'string'
9
+ && marker.length > 0;
10
+ }
11
+ delete(record) {
12
+ if (this.isDeleted(record))
13
+ return record;
14
+ return {
15
+ ...record,
16
+ [this.deletedField]: crypto.randomUUID()
17
+ };
18
+ }
19
+ mapIndexQuery(index, key) {
20
+ return this.mapActiveIndexQuery?.(index, key)
21
+ ?? null;
22
+ }
23
+ }
@@ -0,0 +1,8 @@
1
+ export interface DeleteStrategy<T extends Record<string, any>> {
2
+ isDeleted(record: T): boolean;
3
+ delete(record: T): T;
4
+ mapIndexQuery?(index: string, key: IDBValidKey): {
5
+ index: string;
6
+ key: IDBValidKey;
7
+ } | null;
8
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export { dbDelete, dbOpen, dbRequestAsync, } from './db.js';
2
2
  export { IncrementVersionStrategy as IncrementTableVersionStrategy, } from './version-strategy-increment.js';
3
+ export { type DeleteStrategy as TableDeleteStrategy, } from './delete-strategy.js';
3
4
  export { Table, type TableEvents, type TableEventsReceiver, } from './table.js';
4
5
  export { VersionConflictError as TableVersionConflictError, } from './version-conflict-error.js';
5
6
  export { type VersionStrategy as TableVersionStrategy, } from './version-strategy.js';
6
7
  export { txDone, txRead, txReuseOrCreate, txWrite, TxMode, txEnsure, } from './transactions.js';
8
+ export { UuidSoftDeleteStrategy as UuidSoftDeleteTableDeleteStrategy, } from './delete-strategy-uuid-soft-delete-strategy.js';
7
9
  export { UuidVersionStrategy as UuidTableVersionStrategy, } from './version-strategy-uuid.js';
package/dist/index.js CHANGED
@@ -3,4 +3,5 @@ export { IncrementVersionStrategy as IncrementTableVersionStrategy, } from './ve
3
3
  export { Table, } from './table.js';
4
4
  export { VersionConflictError as TableVersionConflictError, } from './version-conflict-error.js';
5
5
  export { txDone, txRead, txReuseOrCreate, txWrite, TxMode, txEnsure, } from './transactions.js';
6
+ export { UuidSoftDeleteStrategy as UuidSoftDeleteTableDeleteStrategy, } from './delete-strategy-uuid-soft-delete-strategy.js';
6
7
  export { UuidVersionStrategy as UuidTableVersionStrategy, } from './version-strategy-uuid.js';
package/dist/table.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { EventfulBase } from 'asljs-eventful';
2
2
  import { type KeyPath } from './keys.js';
3
+ import { type DeleteStrategy } from './delete-strategy.js';
3
4
  import { type VersionStrategy } from './version-strategy.js';
4
5
  export type TableEvents<T extends Record<string, any>> = {
5
6
  add: [record: T];
@@ -15,7 +16,10 @@ export declare class Table<T extends Record<string, any>> extends EventfulBase<T
15
16
  readonly storeName: string;
16
17
  readonly db: IDBDatabase;
17
18
  readonly key: KeyPath<T>;
18
- constructor(storeName: string, db: IDBDatabase, strategy?: VersionStrategy<T>);
19
+ constructor(storeName: string, db: IDBDatabase, options?: {
20
+ versionStrategy?: VersionStrategy<T>;
21
+ deleteStrategy?: DeleteStrategy<T>;
22
+ });
19
23
  getOne(key: IDBValidKey, tx?: IDBTransaction | null): Promise<T | null>;
20
24
  get(index: string, key: IDBValidKey, tx?: IDBTransaction | null): Promise<T[]>;
21
25
  notify(receiver: TableEventsReceiver<T>): () => boolean;
package/dist/table.js CHANGED
@@ -9,21 +9,23 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _Table_instances, _Table_receivers, _Table_strategy, _Table_onTransactionCompleted, _Table_notify;
12
+ var _Table_instances, _Table_receivers, _Table_versionStrategy, _Table_deleteStrategy, _Table_activeRecords, _Table_getActiveByIndex, _Table_getByIndex, _Table_isDeleted, _Table_canFallbackMappedQuery, _Table_onTransactionCompleted, _Table_notify;
13
13
  import { EventfulBase } from 'asljs-eventful';
14
14
  import { dbRequestAsync } from './db.js';
15
15
  import { keyAssert, keyGet, keyPathAssert, } from './keys.js';
16
16
  import { VersionConflictError } from './version-conflict-error.js';
17
17
  import { txDone, txRead, txWrite, } from './transactions.js';
18
18
  export class Table extends EventfulBase {
19
- constructor(storeName, db, strategy) {
19
+ constructor(storeName, db, options) {
20
20
  super();
21
21
  _Table_instances.add(this);
22
22
  this.storeName = storeName;
23
23
  this.db = db;
24
24
  _Table_receivers.set(this, []);
25
- _Table_strategy.set(this, void 0);
26
- __classPrivateFieldSet(this, _Table_strategy, strategy, "f");
25
+ _Table_versionStrategy.set(this, void 0);
26
+ _Table_deleteStrategy.set(this, void 0);
27
+ __classPrivateFieldSet(this, _Table_versionStrategy, options?.versionStrategy, "f");
28
+ __classPrivateFieldSet(this, _Table_deleteStrategy, options?.deleteStrategy, "f");
27
29
  const key = txRead(this.db, this.storeName)
28
30
  .objectStore(this.storeName)
29
31
  .keyPath;
@@ -43,6 +45,10 @@ export class Table extends EventfulBase {
43
45
  resolve(null);
44
46
  return;
45
47
  }
48
+ if (__classPrivateFieldGet(this, _Table_instances, "m", _Table_isDeleted).call(this, fields)) {
49
+ resolve(null);
50
+ return;
51
+ }
46
52
  resolve(fields);
47
53
  };
48
54
  request.onerror =
@@ -53,25 +59,9 @@ export class Table extends EventfulBase {
53
59
  });
54
60
  }
55
61
  get(index, key, tx = null) {
56
- const store = txRead(this.db, this.storeName, tx)
57
- .objectStore(this.storeName);
58
- const idx = store.index(index);
59
- const keyPath = idx.keyPath;
60
- keyPathAssert(keyPath);
61
- keyAssert(keyPath, key);
62
- return new Promise((resolve, reject) => {
63
- const request = idx.getAll(key);
64
- request.onsuccess =
65
- () => {
66
- const records = request.result;
67
- resolve(records);
68
- };
69
- request.onerror =
70
- () => {
71
- reject(request.error
72
- ?? new Error(`${this.storeName}: get request failed for index ${index}`));
73
- };
74
- });
62
+ if (__classPrivateFieldGet(this, _Table_deleteStrategy, "f") === undefined)
63
+ return __classPrivateFieldGet(this, _Table_instances, "m", _Table_getByIndex).call(this, index, key, tx);
64
+ return __classPrivateFieldGet(this, _Table_instances, "m", _Table_getActiveByIndex).call(this, index, key, tx);
75
65
  }
76
66
  notify(receiver) {
77
67
  __classPrivateFieldGet(this, _Table_receivers, "f").push(receiver);
@@ -87,7 +77,7 @@ export class Table extends EventfulBase {
87
77
  const records = await dbRequestAsync(txRead(this.db, this.storeName, tx)
88
78
  .objectStore(this.storeName)
89
79
  .getAll());
90
- return records;
80
+ return __classPrivateFieldGet(this, _Table_instances, "m", _Table_activeRecords).call(this, records);
91
81
  }
92
82
  scan(predicate, tx = null) {
93
83
  return new Promise((resolve, reject) => {
@@ -103,6 +93,10 @@ export class Table extends EventfulBase {
103
93
  return;
104
94
  }
105
95
  const record = cursor.value;
96
+ if (__classPrivateFieldGet(this, _Table_instances, "m", _Table_isDeleted).call(this, record)) {
97
+ cursor.continue();
98
+ return;
99
+ }
106
100
  try {
107
101
  if (predicate(record)) {
108
102
  result.push(record);
@@ -122,8 +116,8 @@ export class Table extends EventfulBase {
122
116
  });
123
117
  }
124
118
  async add(record, tx = null) {
125
- const storedRecord = __classPrivateFieldGet(this, _Table_strategy, "f")
126
- ? __classPrivateFieldGet(this, _Table_strategy, "f").initialise(record)
119
+ const storedRecord = __classPrivateFieldGet(this, _Table_versionStrategy, "f")
120
+ ? __classPrivateFieldGet(this, _Table_versionStrategy, "f").initialise(record)
127
121
  : record;
128
122
  const ltx = txWrite(this.db, this.storeName, tx);
129
123
  const store = ltx.objectStore(this.storeName);
@@ -145,14 +139,14 @@ export class Table extends EventfulBase {
145
139
  throw new Error(`Record with key ${String(key)} not found.`);
146
140
  }
147
141
  let storedRecord = record;
148
- if (__classPrivateFieldGet(this, _Table_strategy, "f")) {
142
+ if (__classPrivateFieldGet(this, _Table_versionStrategy, "f")) {
149
143
  if (expectedVersion === undefined) {
150
144
  throw new Error(`${this.storeName}: expectedVersion is required when a version strategy is configured.`);
151
145
  }
152
- if (!__classPrivateFieldGet(this, _Table_strategy, "f").verify(existing, expectedVersion)) {
153
- throw new VersionConflictError(key, expectedVersion, __classPrivateFieldGet(this, _Table_strategy, "f").getVersion(existing));
146
+ if (!__classPrivateFieldGet(this, _Table_versionStrategy, "f").verify(existing, expectedVersion)) {
147
+ throw new VersionConflictError(key, expectedVersion, __classPrivateFieldGet(this, _Table_versionStrategy, "f").getVersion(existing));
154
148
  }
155
- storedRecord = __classPrivateFieldGet(this, _Table_strategy, "f").update(record);
149
+ storedRecord = __classPrivateFieldGet(this, _Table_versionStrategy, "f").update(record);
156
150
  }
157
151
  await dbRequestAsync(store.put(storedRecord));
158
152
  __classPrivateFieldGet(this, _Table_instances, "m", _Table_onTransactionCompleted).call(this, store.transaction, () => {
@@ -171,18 +165,41 @@ export class Table extends EventfulBase {
171
165
  const existing = await dbRequestAsync(store.get(key));
172
166
  if (existing === undefined)
173
167
  return;
174
- if (__classPrivateFieldGet(this, _Table_strategy, "f")) {
168
+ if (__classPrivateFieldGet(this, _Table_deleteStrategy, "f") === undefined) {
169
+ if (__classPrivateFieldGet(this, _Table_versionStrategy, "f")) {
170
+ if (expectedVersion === undefined) {
171
+ throw new Error(`${this.storeName}: expectedVersion is required when a version strategy is configured.`);
172
+ }
173
+ if (!__classPrivateFieldGet(this, _Table_versionStrategy, "f").verify(existing, expectedVersion)) {
174
+ throw new VersionConflictError(key, expectedVersion, __classPrivateFieldGet(this, _Table_versionStrategy, "f").getVersion(existing));
175
+ }
176
+ }
177
+ await dbRequestAsync(store.delete(key));
178
+ __classPrivateFieldGet(this, _Table_instances, "m", _Table_onTransactionCompleted).call(this, store.transaction, () => {
179
+ this.emit('delete', existing);
180
+ __classPrivateFieldGet(this, _Table_instances, "m", _Table_notify).call(this, 'delete', [existing]);
181
+ });
182
+ if (tx === null)
183
+ await txDone(ltx);
184
+ return;
185
+ }
186
+ if (__classPrivateFieldGet(this, _Table_deleteStrategy, "f").isDeleted(existing))
187
+ return;
188
+ if (__classPrivateFieldGet(this, _Table_versionStrategy, "f")) {
175
189
  if (expectedVersion === undefined) {
176
190
  throw new Error(`${this.storeName}: expectedVersion is required when a version strategy is configured.`);
177
191
  }
178
- if (!__classPrivateFieldGet(this, _Table_strategy, "f").verify(existing, expectedVersion)) {
179
- throw new VersionConflictError(key, expectedVersion, __classPrivateFieldGet(this, _Table_strategy, "f").getVersion(existing));
192
+ if (!__classPrivateFieldGet(this, _Table_versionStrategy, "f").verify(existing, expectedVersion)) {
193
+ throw new VersionConflictError(key, expectedVersion, __classPrivateFieldGet(this, _Table_versionStrategy, "f").getVersion(existing));
180
194
  }
181
195
  }
182
- await dbRequestAsync(store.delete(key));
196
+ let storedRecord = __classPrivateFieldGet(this, _Table_deleteStrategy, "f").delete(existing);
197
+ if (__classPrivateFieldGet(this, _Table_versionStrategy, "f"))
198
+ storedRecord = __classPrivateFieldGet(this, _Table_versionStrategy, "f").update(storedRecord);
199
+ await dbRequestAsync(store.put(storedRecord));
183
200
  __classPrivateFieldGet(this, _Table_instances, "m", _Table_onTransactionCompleted).call(this, store.transaction, () => {
184
- this.emit('delete', existing);
185
- __classPrivateFieldGet(this, _Table_instances, "m", _Table_notify).call(this, 'delete', [existing]);
201
+ this.emit('delete', storedRecord);
202
+ __classPrivateFieldGet(this, _Table_instances, "m", _Table_notify).call(this, 'delete', [storedRecord]);
186
203
  });
187
204
  if (tx === null)
188
205
  await txDone(ltx);
@@ -200,7 +217,57 @@ export class Table extends EventfulBase {
200
217
  await txDone(ltx);
201
218
  }
202
219
  }
203
- _Table_receivers = new WeakMap(), _Table_strategy = new WeakMap(), _Table_instances = new WeakSet(), _Table_onTransactionCompleted = function _Table_onTransactionCompleted(tx, action) {
220
+ _Table_receivers = new WeakMap(), _Table_versionStrategy = new WeakMap(), _Table_deleteStrategy = new WeakMap(), _Table_instances = new WeakSet(), _Table_activeRecords = function _Table_activeRecords(records) {
221
+ const deleteStrategy = __classPrivateFieldGet(this, _Table_deleteStrategy, "f");
222
+ if (deleteStrategy === undefined)
223
+ return records;
224
+ return records.filter(record => !deleteStrategy.isDeleted(record));
225
+ }, _Table_getActiveByIndex = async function _Table_getActiveByIndex(index, key, tx) {
226
+ const mapped = __classPrivateFieldGet(this, _Table_deleteStrategy, "f")?.mapIndexQuery?.(index, key);
227
+ if (mapped) {
228
+ try {
229
+ const records = await __classPrivateFieldGet(this, _Table_instances, "m", _Table_getByIndex).call(this, mapped.index, mapped.key, tx, false);
230
+ return __classPrivateFieldGet(this, _Table_instances, "m", _Table_activeRecords).call(this, records);
231
+ }
232
+ catch (error) {
233
+ // Degrade to filtering when mapped query cannot be executed.
234
+ if (!__classPrivateFieldGet(this, _Table_instances, "m", _Table_canFallbackMappedQuery).call(this, error))
235
+ throw error;
236
+ }
237
+ }
238
+ const records = await __classPrivateFieldGet(this, _Table_instances, "m", _Table_getByIndex).call(this, index, key, tx);
239
+ return __classPrivateFieldGet(this, _Table_instances, "m", _Table_activeRecords).call(this, records);
240
+ }, _Table_getByIndex = function _Table_getByIndex(index, key, tx, validateKey = true) {
241
+ const store = txRead(this.db, this.storeName, tx)
242
+ .objectStore(this.storeName);
243
+ const idx = store.index(index);
244
+ const keyPath = idx.keyPath;
245
+ if (validateKey) {
246
+ keyPathAssert(keyPath);
247
+ keyAssert(keyPath, key);
248
+ }
249
+ return new Promise((resolve, reject) => {
250
+ const request = idx.getAll(key);
251
+ request.onsuccess =
252
+ () => {
253
+ const records = request.result;
254
+ resolve(records);
255
+ };
256
+ request.onerror =
257
+ () => {
258
+ reject(request.error
259
+ ?? new Error(`${this.storeName}: get request failed for index ${index}`));
260
+ };
261
+ });
262
+ }, _Table_isDeleted = function _Table_isDeleted(record) {
263
+ return __classPrivateFieldGet(this, _Table_deleteStrategy, "f")?.isDeleted(record)
264
+ ?? false;
265
+ }, _Table_canFallbackMappedQuery = function _Table_canFallbackMappedQuery(error) {
266
+ if (!(error instanceof DOMException))
267
+ return false;
268
+ return error.name === 'NotFoundError'
269
+ || error.name === 'DataError';
270
+ }, _Table_onTransactionCompleted = function _Table_onTransactionCompleted(tx, action) {
204
271
  tx.addEventListener('complete', () => {
205
272
  try {
206
273
  action();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "asljs-dali",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "IndexedDB data layer with a typed Table abstraction.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",