locality-idb 0.4.0 → 0.5.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/dist/index.cjs +741 -0
- package/dist/index.d.cts +688 -0
- package/dist/index.d.mts +405 -121
- package/dist/index.iife.js +745 -0
- package/dist/index.mjs +309 -431
- package/dist/index.umd.js +747 -0
- package/package.json +9 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
|
|
2
|
+
//#region node_modules/.pnpm/nhb-toolbox@4.28.66/node_modules/nhb-toolbox/dist/esm/guards/primitives.js
|
|
3
|
+
function isNumber(value) {
|
|
4
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
5
|
+
}
|
|
6
|
+
function isString(value) {
|
|
7
|
+
return typeof value === "string";
|
|
8
|
+
}
|
|
9
|
+
function isInteger(value) {
|
|
10
|
+
return isNumber(value) && Number.isInteger(value);
|
|
11
|
+
}
|
|
12
|
+
function isPositiveInteger(value) {
|
|
13
|
+
return isInteger(value) && value > 0;
|
|
14
|
+
}
|
|
15
|
+
function isBoolean(value) {
|
|
16
|
+
return typeof value === "boolean";
|
|
17
|
+
}
|
|
18
|
+
function isNonEmptyString(value) {
|
|
19
|
+
return isString(value) && value?.length > 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region node_modules/.pnpm/nhb-toolbox@4.28.66/node_modules/nhb-toolbox/dist/esm/guards/non-primitives.js
|
|
24
|
+
function isArray(value) {
|
|
25
|
+
return Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
function isValidArray(value) {
|
|
28
|
+
return Array.isArray(value) && value?.length > 0;
|
|
29
|
+
}
|
|
30
|
+
function isObject(value) {
|
|
31
|
+
return value !== null && typeof value === "object" && !isArray(value);
|
|
32
|
+
}
|
|
33
|
+
function isNotEmptyObject(value) {
|
|
34
|
+
return isObject(value) && Object.keys(value)?.length > 0;
|
|
35
|
+
}
|
|
36
|
+
function isArrayOfType(value, typeCheck) {
|
|
37
|
+
return isArray(value) && value?.every(typeCheck);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region node_modules/.pnpm/nhb-toolbox@4.28.66/node_modules/nhb-toolbox/dist/esm/array/sort.js
|
|
42
|
+
function naturalSort(a, b, options) {
|
|
43
|
+
const { caseInsensitive = true, localeAware = false } = options || {};
|
|
44
|
+
const _createChunks = (str) => {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
let current = "";
|
|
47
|
+
let isNumeric = false;
|
|
48
|
+
for (const char of str) {
|
|
49
|
+
const charIsNum = !Number.isNaN(Number(char));
|
|
50
|
+
if (current?.length === 0) {
|
|
51
|
+
current = char;
|
|
52
|
+
isNumeric = charIsNum;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (charIsNum === isNumeric) current += char;
|
|
56
|
+
else {
|
|
57
|
+
chunks?.push(isNumeric ? Number(current) : current);
|
|
58
|
+
current = char;
|
|
59
|
+
isNumeric = charIsNum;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (current?.length > 0) chunks?.push(isNumeric ? Number(current) : current);
|
|
63
|
+
return chunks;
|
|
64
|
+
};
|
|
65
|
+
const aChunks = _createChunks(a);
|
|
66
|
+
const bChunks = _createChunks(b);
|
|
67
|
+
for (let i = 0; i < Math.min(aChunks?.length, bChunks?.length); i++) {
|
|
68
|
+
let aChunk = aChunks[i];
|
|
69
|
+
let bChunk = bChunks[i];
|
|
70
|
+
if (caseInsensitive && typeof aChunk === "string" && typeof bChunk === "string") {
|
|
71
|
+
aChunk = aChunk?.toLowerCase();
|
|
72
|
+
bChunk = bChunk?.toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
if (typeof aChunk !== typeof bChunk) return typeof aChunk === "string" ? 1 : -1;
|
|
75
|
+
if (aChunk !== bChunk) {
|
|
76
|
+
if (typeof aChunk === "number" && typeof bChunk === "number") return aChunk - bChunk;
|
|
77
|
+
if (typeof aChunk === "string" && typeof bChunk === "string") {
|
|
78
|
+
if (localeAware) {
|
|
79
|
+
const cmp = aChunk.localeCompare(bChunk, void 0, { sensitivity: caseInsensitive ? "accent" : "variant" });
|
|
80
|
+
if (cmp !== 0) return cmp;
|
|
81
|
+
}
|
|
82
|
+
return aChunk < bChunk ? -1 : 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return aChunks?.length - bChunks?.length;
|
|
87
|
+
}
|
|
88
|
+
function sortAnArray(array, options) {
|
|
89
|
+
if (!isValidArray(array)) return array;
|
|
90
|
+
if (isArrayOfType(array, isString)) return [...array].sort((a, b) => options?.sortOrder === "desc" ? naturalSort(b, a) : naturalSort(a, b));
|
|
91
|
+
if (isArrayOfType(array, isNumber)) return [...array].sort((a, b) => options?.sortOrder === "desc" ? b - a : a - b);
|
|
92
|
+
if (isArrayOfType(array, isBoolean)) return [...array].sort((a, b) => options?.sortOrder === "desc" ? Number(b) - Number(a) : Number(a) - Number(b));
|
|
93
|
+
if (isArrayOfType(array, isObject) && options && "sortByField" in options) return [...array].sort((a, b) => {
|
|
94
|
+
const _getKeyValue = (obj, path) => {
|
|
95
|
+
return path.split(".").reduce((acc, key) => acc?.[key], obj);
|
|
96
|
+
};
|
|
97
|
+
const keyA = _getKeyValue(a, options?.sortByField);
|
|
98
|
+
const keyB = _getKeyValue(b, options?.sortByField);
|
|
99
|
+
if (keyA == null || keyB == null) return keyA == null ? 1 : -1;
|
|
100
|
+
if (isString(keyA) && isString(keyB)) return options?.sortOrder === "desc" ? naturalSort(keyB, keyA) : naturalSort(keyA, keyB);
|
|
101
|
+
if (isNumber(keyA) && isNumber(keyB)) return options?.sortOrder === "desc" ? keyB - keyA : keyA - keyB;
|
|
102
|
+
if (isBoolean(keyA) && isBoolean(keyB)) return options?.sortOrder === "desc" ? Number(keyB) - Number(keyA) : Number(keyA) - Number(keyB);
|
|
103
|
+
return 0;
|
|
104
|
+
});
|
|
105
|
+
return [...array];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/core.ts
|
|
110
|
+
/** Symbol key for column column data type */
|
|
111
|
+
const ColumnType = Symbol("ColumnType");
|
|
112
|
+
/** Symbol key for primary key marker */
|
|
113
|
+
const IsPrimaryKey = Symbol("IsPrimaryKey");
|
|
114
|
+
/** Symbol key for auto increment marker */
|
|
115
|
+
const IsAutoInc = Symbol("IsAutoInc");
|
|
116
|
+
/** Symbol key for optional marker */
|
|
117
|
+
const IsOptional = Symbol("IsOptional");
|
|
118
|
+
/** Symbol key for indexed marker */
|
|
119
|
+
const IsIndexed = Symbol("IsIndexed");
|
|
120
|
+
/** Symbol key for unique marker */
|
|
121
|
+
const IsUnique = Symbol("IsUnique");
|
|
122
|
+
/** Symbol key for default value */
|
|
123
|
+
const DefaultValue = Symbol("DefaultValue");
|
|
124
|
+
/** @class Represents a column definition. */
|
|
125
|
+
var Column = class {
|
|
126
|
+
constructor(type) {
|
|
127
|
+
this[ColumnType] = type;
|
|
128
|
+
}
|
|
129
|
+
/** @instance Marks column as primary key */
|
|
130
|
+
pk() {
|
|
131
|
+
this[IsPrimaryKey] = true;
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
/** @instance Marks column as unique */
|
|
135
|
+
unique() {
|
|
136
|
+
this[IsIndexed] = true;
|
|
137
|
+
this[IsUnique] = true;
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
/** @instance Enables auto increment - only available for numeric columns */
|
|
141
|
+
auto() {
|
|
142
|
+
const colType = this[ColumnType];
|
|
143
|
+
if (!isNonEmptyString(colType) || ![
|
|
144
|
+
"int",
|
|
145
|
+
"integer",
|
|
146
|
+
"float",
|
|
147
|
+
"number"
|
|
148
|
+
].includes(colType.toLowerCase())) throw new Error(`auto() can only be used with integer columns, got: ${colType}`);
|
|
149
|
+
this[IsAutoInc] = true;
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
/** @instance Marks column as indexed */
|
|
153
|
+
index() {
|
|
154
|
+
this[IsIndexed] = true;
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
/** @instance Sets default value for the column */
|
|
158
|
+
default(value) {
|
|
159
|
+
this[DefaultValue] = value;
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
/** @instance Marks column as optional */
|
|
163
|
+
optional() {
|
|
164
|
+
this[IsOptional] = true;
|
|
165
|
+
return this;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
/** @class Represents a table. */
|
|
169
|
+
var Table = class {
|
|
170
|
+
name;
|
|
171
|
+
columns;
|
|
172
|
+
constructor(name, columns) {
|
|
173
|
+
this.name = name;
|
|
174
|
+
this.columns = columns;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/factory.ts
|
|
180
|
+
/**
|
|
181
|
+
* * Opens an IndexedDB database with the specified stores.
|
|
182
|
+
* @param name Database name
|
|
183
|
+
* @param stores Array of store configurations
|
|
184
|
+
* @param version Database version (default is `1`)
|
|
185
|
+
* @returns Promise that resolves to the opened {@link IDBDatabase} instance.
|
|
186
|
+
*/
|
|
187
|
+
function openDBWithStores(name, stores, version = 1) {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
if (!window.indexedDB) throw new Error("IndexedDb is not supported in this environment or browser!");
|
|
190
|
+
const request = window.indexedDB.open(name, version);
|
|
191
|
+
request.onupgradeneeded = (event) => {
|
|
192
|
+
const db = event.target.result;
|
|
193
|
+
for (const store of stores) if (!db.objectStoreNames.contains(store.name)) db.createObjectStore(store.name, {
|
|
194
|
+
keyPath: store.keyPath,
|
|
195
|
+
autoIncrement: store.autoIncrement
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
request.onsuccess = () => resolve(request.result);
|
|
199
|
+
request.onerror = () => reject(request.error);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/helpers.ts
|
|
205
|
+
/** Ensure UUID variant is RFC4122 compliant */
|
|
206
|
+
function _hexVariant(hex) {
|
|
207
|
+
return (parseInt(hex, 16) & 63 | 128).toString(16).padStart(2, "0");
|
|
208
|
+
}
|
|
209
|
+
/** Convert a hex string to UUID format */
|
|
210
|
+
function _formatUUID(h, v, up) {
|
|
211
|
+
const part3 = String(v) + h.slice(13, 16);
|
|
212
|
+
const part4 = _hexVariant(h.slice(16, 18)) + h.slice(18, 20);
|
|
213
|
+
const formatted = [
|
|
214
|
+
h.slice(0, 8),
|
|
215
|
+
h.slice(8, 12),
|
|
216
|
+
part3,
|
|
217
|
+
part4,
|
|
218
|
+
h.slice(20, 32)
|
|
219
|
+
].join("-");
|
|
220
|
+
return up ? formatted.toUpperCase() : formatted;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/utils.ts
|
|
225
|
+
/**
|
|
226
|
+
* * Generate a random UUID v4 string
|
|
227
|
+
* @param uppercase Whether to return the UUID in uppercase format. Default is `false`.
|
|
228
|
+
* @returns UUID v4 string
|
|
229
|
+
*/
|
|
230
|
+
function uuidV4(uppercase = false) {
|
|
231
|
+
const bytes = new Uint8Array(16);
|
|
232
|
+
for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
233
|
+
let hex = "";
|
|
234
|
+
for (let i = 0; i < 16; i++) hex += bytes[i].toString(16).padStart(2, "0");
|
|
235
|
+
return _formatUUID(hex, 4, uppercase);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* * Get current timestamp in ISO 8601 format
|
|
239
|
+
* @param date Optional Date object to format. Defaults to current {@link Date new Date()}
|
|
240
|
+
* @return Timestamp string in ISO 8601 format
|
|
241
|
+
*/
|
|
242
|
+
function getTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
243
|
+
return date.toISOString();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/query.ts
|
|
248
|
+
/** Symbol for type extraction (exists only in type system) */
|
|
249
|
+
const Selected = Symbol("Selected");
|
|
250
|
+
/** Symbol to indicate if insert data is array */
|
|
251
|
+
const IsArray = Symbol("IsArray");
|
|
252
|
+
/**
|
|
253
|
+
* @class Select query builder.
|
|
254
|
+
*/
|
|
255
|
+
var SelectQuery = class {
|
|
256
|
+
#table;
|
|
257
|
+
#readyPromise;
|
|
258
|
+
#dbGetter;
|
|
259
|
+
#whereCondition;
|
|
260
|
+
#orderByKey;
|
|
261
|
+
#orderByDir = "asc";
|
|
262
|
+
#limitCount;
|
|
263
|
+
constructor(table, dbGetter, readyPromise) {
|
|
264
|
+
this.#table = table;
|
|
265
|
+
this.#dbGetter = dbGetter;
|
|
266
|
+
this.#readyPromise = readyPromise;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* @instance Select or exclude specific columns
|
|
270
|
+
* @param cols Columns to select or exclude
|
|
271
|
+
*/
|
|
272
|
+
select(cols) {
|
|
273
|
+
this[Selected] = cols;
|
|
274
|
+
return this;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* @instance Filter rows based on predicate function
|
|
278
|
+
* @param predicate Filtering function
|
|
279
|
+
*/
|
|
280
|
+
where(predicate) {
|
|
281
|
+
this.#whereCondition = predicate;
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* @instance Order results by specified key and direction
|
|
286
|
+
* @param key Key to order by
|
|
287
|
+
* @param dir Direction: 'asc' | 'desc' (default: 'asc')
|
|
288
|
+
*/
|
|
289
|
+
orderBy(key, dir = "asc") {
|
|
290
|
+
this.#orderByKey = key;
|
|
291
|
+
this.#orderByDir = dir;
|
|
292
|
+
return this;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* @instance Limit number of results
|
|
296
|
+
* @param count Maximum number of results to return
|
|
297
|
+
*/
|
|
298
|
+
limit(count) {
|
|
299
|
+
this.#limitCount = count;
|
|
300
|
+
return this;
|
|
301
|
+
}
|
|
302
|
+
/** Projects a row based on selected fields */
|
|
303
|
+
#projectRow(row) {
|
|
304
|
+
if (!isNotEmptyObject(this[Selected])) return row;
|
|
305
|
+
const projected = {};
|
|
306
|
+
const selectionEntries = Object.entries(this[Selected]);
|
|
307
|
+
const selectionKeys = new Set(Object.keys(this[Selected]));
|
|
308
|
+
if (selectionEntries.some(([, value]) => value === true)) {
|
|
309
|
+
for (const [key, value] of selectionEntries) if (value === true) projected[key] = row[key];
|
|
310
|
+
} else for (const key of Object.keys(row)) if (!selectionKeys.has(key) || this[Selected][key] !== false) projected[key] = row[key];
|
|
311
|
+
return projected;
|
|
312
|
+
}
|
|
313
|
+
async all() {
|
|
314
|
+
await this.#readyPromise;
|
|
315
|
+
return new Promise((resolve, reject) => {
|
|
316
|
+
const request = this.#dbGetter().transaction(this.#table, "readonly").objectStore(this.#table).getAll();
|
|
317
|
+
request.onsuccess = () => {
|
|
318
|
+
let results = request.result;
|
|
319
|
+
if (this.#whereCondition) results = results.filter(this.#whereCondition);
|
|
320
|
+
if (this.#orderByKey) results = sortAnArray(results, {
|
|
321
|
+
sortOrder: this.#orderByDir,
|
|
322
|
+
sortByField: this.#orderByKey
|
|
323
|
+
});
|
|
324
|
+
if (this.#limitCount) results = results.slice(0, this.#limitCount);
|
|
325
|
+
resolve(results.map((row) => this.#projectRow(row)));
|
|
326
|
+
};
|
|
327
|
+
request.onerror = () => reject(request.error);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
async first() {
|
|
331
|
+
await this.#readyPromise;
|
|
332
|
+
return new Promise((resolve, reject) => {
|
|
333
|
+
const request = this.#dbGetter().transaction(this.#table, "readonly").objectStore(this.#table).getAll();
|
|
334
|
+
request.onsuccess = () => {
|
|
335
|
+
let results = request.result;
|
|
336
|
+
if (this.#whereCondition) results = results.filter(this.#whereCondition);
|
|
337
|
+
if (results.length > 0) resolve(this.#projectRow(results[0]));
|
|
338
|
+
else resolve(null);
|
|
339
|
+
};
|
|
340
|
+
request.onerror = () => reject(request.error);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
/** @class Insert query builder. */
|
|
345
|
+
var InsertQuery = class {
|
|
346
|
+
#table;
|
|
347
|
+
#dbGetter;
|
|
348
|
+
#readyPromise;
|
|
349
|
+
#dataToInsert = [];
|
|
350
|
+
#columns;
|
|
351
|
+
constructor(table, dbGetter, readyPromise, columns) {
|
|
352
|
+
this.#table = table;
|
|
353
|
+
this.#dbGetter = dbGetter;
|
|
354
|
+
this.#readyPromise = readyPromise;
|
|
355
|
+
this.#columns = columns;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* @instance Sets the data to be inserted
|
|
359
|
+
* @param data Data object or array of data objects to insert
|
|
360
|
+
*/
|
|
361
|
+
values(data) {
|
|
362
|
+
this[IsArray] = Array.isArray(data);
|
|
363
|
+
this.#dataToInsert = this[IsArray] ? data : [data];
|
|
364
|
+
return this;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* @instance Executes the insert query
|
|
368
|
+
* @returns Inserted record(s)
|
|
369
|
+
*/
|
|
370
|
+
async run() {
|
|
371
|
+
await this.#readyPromise;
|
|
372
|
+
return new Promise((resolve, reject) => {
|
|
373
|
+
const transaction = this.#dbGetter().transaction(this.#table, "readwrite");
|
|
374
|
+
const store = transaction.objectStore(this.#table);
|
|
375
|
+
const insertedDocs = [];
|
|
376
|
+
const promises = this.#dataToInsert.map((data) => {
|
|
377
|
+
return new Promise((res, rej) => {
|
|
378
|
+
const updated = { ...data };
|
|
379
|
+
if (this.#columns) Object.entries(this.#columns).forEach(([fieldName, column]) => {
|
|
380
|
+
const defaultValue = column[DefaultValue];
|
|
381
|
+
if (!(fieldName in updated) && defaultValue !== void 0) updated[fieldName] = defaultValue;
|
|
382
|
+
const columnType = column[ColumnType];
|
|
383
|
+
if (columnType === "uuid" && !(fieldName in updated)) updated[fieldName] = uuidV4();
|
|
384
|
+
if (columnType === "timestamp" && !(fieldName in updated)) updated[fieldName] = getTimestamp();
|
|
385
|
+
});
|
|
386
|
+
const request = store.add(updated);
|
|
387
|
+
request.onsuccess = () => {
|
|
388
|
+
const key = request.result;
|
|
389
|
+
const getRequest = store.get(key);
|
|
390
|
+
getRequest.onsuccess = () => {
|
|
391
|
+
insertedDocs.push(getRequest.result);
|
|
392
|
+
res();
|
|
393
|
+
};
|
|
394
|
+
getRequest.onerror = () => rej(getRequest.error);
|
|
395
|
+
};
|
|
396
|
+
request.onerror = () => rej(request.error);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
transaction.oncomplete = () => Promise.all(promises).then(() => this[IsArray] === true ? resolve(insertedDocs) : resolve(insertedDocs[0])).catch(reject);
|
|
400
|
+
transaction.onerror = () => reject(transaction.error);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
/** @class Update query builder. */
|
|
405
|
+
var UpdateQuery = class {
|
|
406
|
+
#table;
|
|
407
|
+
#dbGetter;
|
|
408
|
+
#readyPromise;
|
|
409
|
+
#dataToUpdate;
|
|
410
|
+
#whereCondition;
|
|
411
|
+
constructor(table, dbGetter, readyPromise) {
|
|
412
|
+
this.#table = table;
|
|
413
|
+
this.#dbGetter = dbGetter;
|
|
414
|
+
this.#readyPromise = readyPromise;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* @instance Sets the data to be updated
|
|
418
|
+
* @param values Values to update
|
|
419
|
+
*/
|
|
420
|
+
set(values) {
|
|
421
|
+
this.#dataToUpdate = values;
|
|
422
|
+
return this;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* @instance Filter rows to update
|
|
426
|
+
* @param predicate Filtering function
|
|
427
|
+
*/
|
|
428
|
+
where(predicate) {
|
|
429
|
+
this.#whereCondition = predicate;
|
|
430
|
+
return this;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* @instance Executes the update query
|
|
434
|
+
* @returns Number of records updated
|
|
435
|
+
*/
|
|
436
|
+
async run() {
|
|
437
|
+
await this.#readyPromise;
|
|
438
|
+
if (!isNotEmptyObject(this.#dataToUpdate)) throw new Error("No values set for update!");
|
|
439
|
+
return new Promise((resolve, reject) => {
|
|
440
|
+
const store = this.#dbGetter().transaction(this.#table, "readwrite").objectStore(this.#table);
|
|
441
|
+
const request = store.getAll();
|
|
442
|
+
let updateCount = 0;
|
|
443
|
+
request.onsuccess = () => {
|
|
444
|
+
let rows = request.result;
|
|
445
|
+
if (this.#whereCondition) rows = rows.filter(this.#whereCondition);
|
|
446
|
+
const updatePromises = rows.map((row) => {
|
|
447
|
+
return new Promise((res, rej) => {
|
|
448
|
+
const updatedRow = {
|
|
449
|
+
...row,
|
|
450
|
+
...this.#dataToUpdate
|
|
451
|
+
};
|
|
452
|
+
const putRequest = store.put(updatedRow);
|
|
453
|
+
putRequest.onsuccess = () => {
|
|
454
|
+
updateCount++;
|
|
455
|
+
res();
|
|
456
|
+
};
|
|
457
|
+
putRequest.onerror = () => rej(putRequest.error);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
Promise.all(updatePromises).then(() => resolve(updateCount)).catch(reject);
|
|
461
|
+
};
|
|
462
|
+
request.onerror = () => reject(request.error);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
/** @class Delete query builder. */
|
|
467
|
+
var DeleteQuery = class {
|
|
468
|
+
#table;
|
|
469
|
+
#dbGetter;
|
|
470
|
+
#readyPromise;
|
|
471
|
+
#keyField;
|
|
472
|
+
#whereCondition;
|
|
473
|
+
constructor(table, dbGetter, readyPromise, keyField) {
|
|
474
|
+
this.#table = table;
|
|
475
|
+
this.#dbGetter = dbGetter;
|
|
476
|
+
this.#readyPromise = readyPromise;
|
|
477
|
+
this.#keyField = keyField;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* @instance Filter rows to delete
|
|
481
|
+
* @param predicate Filtering function
|
|
482
|
+
*/
|
|
483
|
+
where(predicate) {
|
|
484
|
+
this.#whereCondition = predicate;
|
|
485
|
+
return this;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* @instance Executes the delete query
|
|
489
|
+
* @returns Number of records deleted
|
|
490
|
+
*/
|
|
491
|
+
async run() {
|
|
492
|
+
await this.#readyPromise;
|
|
493
|
+
return new Promise((resolve, reject) => {
|
|
494
|
+
const store = this.#dbGetter().transaction(this.#table, "readwrite").objectStore(this.#table);
|
|
495
|
+
const request = store.getAll();
|
|
496
|
+
let deleteCount = 0;
|
|
497
|
+
request.onsuccess = () => {
|
|
498
|
+
let rows = request.result;
|
|
499
|
+
if (this.#whereCondition) rows = rows.filter(this.#whereCondition);
|
|
500
|
+
const deletePromises = rows.map((row) => {
|
|
501
|
+
return new Promise((res, rej) => {
|
|
502
|
+
const key = row[this.#keyField];
|
|
503
|
+
const delRequest = store.delete(key);
|
|
504
|
+
delRequest.onsuccess = () => {
|
|
505
|
+
deleteCount++;
|
|
506
|
+
res();
|
|
507
|
+
};
|
|
508
|
+
delRequest.onerror = () => rej(delRequest.error);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
Promise.all(deletePromises).then(() => resolve(deleteCount)).catch(reject);
|
|
512
|
+
};
|
|
513
|
+
request.onerror = () => reject(request.error);
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
//#endregion
|
|
519
|
+
//#region src/client.ts
|
|
520
|
+
/**
|
|
521
|
+
* @class `Locality` class for {@link IndexedDB} interactions.
|
|
522
|
+
*
|
|
523
|
+
* @example
|
|
524
|
+
* import { column, defineSchema, Locality } from 'locality-idb';
|
|
525
|
+
*
|
|
526
|
+
* const schema = defineSchema({
|
|
527
|
+
* users: {
|
|
528
|
+
* id: column.int().pk().auto(),
|
|
529
|
+
* name: column.text(),
|
|
530
|
+
* email: column.text().unique(),
|
|
531
|
+
* },
|
|
532
|
+
* });
|
|
533
|
+
*
|
|
534
|
+
* const db = new Locality({
|
|
535
|
+
* dbName: 'my-database',
|
|
536
|
+
* version: 1,
|
|
537
|
+
* schema,
|
|
538
|
+
* });
|
|
539
|
+
*
|
|
540
|
+
* // Optional
|
|
541
|
+
* await db.ready();
|
|
542
|
+
*
|
|
543
|
+
* // Insert a new user
|
|
544
|
+
* const inserted = await db.insert('users').values({ name: 'Alice', email: 'alice@wonderland.mad' }).run();
|
|
545
|
+
*
|
|
546
|
+
* // Get all users
|
|
547
|
+
* const allUsers = await db.from('users').all();
|
|
548
|
+
*
|
|
549
|
+
* // Select users
|
|
550
|
+
* const allAlices = await db.from('users').where((user) => user.email.includes('alice')).all();
|
|
551
|
+
*
|
|
552
|
+
* // Update a user
|
|
553
|
+
* const updated = await db.update('users').set({ name: 'Alice Liddell' }).where((user) => user.id === 1).run();
|
|
554
|
+
*
|
|
555
|
+
* // Delete a user
|
|
556
|
+
* const deleted = await db.delete('users').where((user) => user.id === 1).run();
|
|
557
|
+
*/
|
|
558
|
+
var Locality = class {
|
|
559
|
+
#name;
|
|
560
|
+
#schema;
|
|
561
|
+
#version;
|
|
562
|
+
#db;
|
|
563
|
+
#readyPromise;
|
|
564
|
+
constructor(config) {
|
|
565
|
+
this.#name = config.dbName;
|
|
566
|
+
this.#schema = config.schema;
|
|
567
|
+
this.#version = config.version;
|
|
568
|
+
const store = this.#buildStoresConfig();
|
|
569
|
+
this.#readyPromise = openDBWithStores(this.#name, store, this.#version).then((db) => {
|
|
570
|
+
this.#db = db;
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
/** Build store configurations from schema. */
|
|
574
|
+
#buildStoresConfig() {
|
|
575
|
+
return Object.entries(this.#schema).map(([tableName, table]) => {
|
|
576
|
+
const columns = table.columns;
|
|
577
|
+
const autoInc = Object.values(columns).find((col) => col[IsPrimaryKey])?.[IsAutoInc] || false;
|
|
578
|
+
return {
|
|
579
|
+
name: tableName,
|
|
580
|
+
keyPath: Object.entries(columns).find(([_, col]) => col[IsPrimaryKey])?.[0],
|
|
581
|
+
autoIncrement: autoInc
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
/** @instance Waits for database initialization to complete. */
|
|
586
|
+
async ready() {
|
|
587
|
+
return this.#readyPromise;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* @instance Select records from a table.
|
|
591
|
+
* @param table Table name.
|
|
592
|
+
* @returns
|
|
593
|
+
*/
|
|
594
|
+
from(table) {
|
|
595
|
+
return new SelectQuery(table, () => this.#db, this.#readyPromise);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* @instance Insert records into a table.
|
|
599
|
+
* @param table Table name.
|
|
600
|
+
*/
|
|
601
|
+
insert(table) {
|
|
602
|
+
return new InsertQuery(table, () => this.#db, this.#readyPromise, this.#schema[table].columns);
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* @instance Update records in a table.
|
|
606
|
+
* @param table Table name.
|
|
607
|
+
*/
|
|
608
|
+
update(table) {
|
|
609
|
+
return new UpdateQuery(table, () => this.#db, this.#readyPromise);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* @instance Delete records from a table.
|
|
613
|
+
* @param table Table name.
|
|
614
|
+
*/
|
|
615
|
+
delete(table) {
|
|
616
|
+
const columns = this.#schema[table].columns;
|
|
617
|
+
const keyField = Object.entries(columns).find(([_, col]) => col[IsPrimaryKey])?.[0];
|
|
618
|
+
return new DeleteQuery(table, () => this.#db, this.#readyPromise, keyField);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/schema.ts
|
|
624
|
+
/**
|
|
625
|
+
* * Defines a database schema from a given schema definition.
|
|
626
|
+
* @param schema An object defining the schema, where each key is a table name and each value is a record of {@link column} definitions.
|
|
627
|
+
* @returns An object mapping each table name to its corresponding {@link Table} instance.
|
|
628
|
+
*
|
|
629
|
+
* @example
|
|
630
|
+
* const schema = defineSchema({
|
|
631
|
+
* users: {
|
|
632
|
+
* id: column.int().pk().auto(),
|
|
633
|
+
* name: column.varchar(255).unique(),
|
|
634
|
+
* createdAt: column.timestamp(),
|
|
635
|
+
* isActive: column.bool().default(true),
|
|
636
|
+
* },
|
|
637
|
+
* posts: {
|
|
638
|
+
* id: column.int().pk().auto(),
|
|
639
|
+
* userId: column.int().index(),
|
|
640
|
+
* title: column.varchar(255),
|
|
641
|
+
* content: column.text(),
|
|
642
|
+
* createdAt: column.timestamp(),
|
|
643
|
+
* },
|
|
644
|
+
* });
|
|
645
|
+
*
|
|
646
|
+
* // Infer types:
|
|
647
|
+
*
|
|
648
|
+
* type User = InferSelectType<typeof schema.users>;
|
|
649
|
+
* type InsertUser = InferInsertType<typeof schema.users>;
|
|
650
|
+
* type UpdateUser = InferUpdateType<typeof schema.users>;
|
|
651
|
+
*
|
|
652
|
+
* type Post = InferSelectType<typeof schema.posts>;
|
|
653
|
+
* type InsertPost = InferInsertType<typeof schema.posts>;
|
|
654
|
+
* type UpdatePost = InferUpdateType<typeof schema.posts>;
|
|
655
|
+
*/
|
|
656
|
+
function defineSchema(schema) {
|
|
657
|
+
const result = {};
|
|
658
|
+
for (const [tableName, columns] of Object.entries(schema)) result[tableName] = new Table(tableName, columns);
|
|
659
|
+
return result;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* * Factory function to create a new {@link Table} instance.
|
|
663
|
+
* @param name The name of the table.
|
|
664
|
+
* @param columns An object defining the columns of the table using {@link column} definitions.
|
|
665
|
+
* @returns A new {@link Table} instance representing the table schema.
|
|
666
|
+
*
|
|
667
|
+
* @example
|
|
668
|
+
* const userTable = table('users', {
|
|
669
|
+
* id: column.int().pk().auto(),
|
|
670
|
+
* name: column.varchar(255).unique(),
|
|
671
|
+
* createdAt: column.timestamp(),
|
|
672
|
+
* isActive: column.bool().default(true),
|
|
673
|
+
* });
|
|
674
|
+
*/
|
|
675
|
+
function table(name, columns) {
|
|
676
|
+
return new Table(name, columns);
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* * Column factory with various column types.
|
|
680
|
+
*
|
|
681
|
+
* @remarks
|
|
682
|
+
* - `char` and `varchar` accept an optional length parameter.
|
|
683
|
+
* - `object`, `array`, `list`, `tuple`, `set`, and `map` are generic and can be typed.
|
|
684
|
+
* - `custom` can be used for any custom data type.
|
|
685
|
+
* - Each column can be further configured using methods like `pk()`, `unique()`, `auto()`, `index()`, `default()`, and `optional()`.
|
|
686
|
+
* - Example usage is provided below:
|
|
687
|
+
*
|
|
688
|
+
* @example
|
|
689
|
+
* const idColumn = column.int().pk().auto();
|
|
690
|
+
* const nameColumn = column.varchar(255).unique();
|
|
691
|
+
* const createdAtColumn = column.timestamp();
|
|
692
|
+
* const isActiveColumn = column.bool().default(true);
|
|
693
|
+
*
|
|
694
|
+
* // Define a table schema
|
|
695
|
+
* const userTable = table('users', {
|
|
696
|
+
* id: idColumn,
|
|
697
|
+
* name: nameColumn,
|
|
698
|
+
* createdAt: createdAtColumn,
|
|
699
|
+
* isActive: isActiveColumn,
|
|
700
|
+
* });
|
|
701
|
+
*
|
|
702
|
+
* // Define a database schema
|
|
703
|
+
* const schema = defineSchema({
|
|
704
|
+
* users: {
|
|
705
|
+
* id: idColumn,
|
|
706
|
+
* name: nameColumn,
|
|
707
|
+
* createdAt: createdAtColumn,
|
|
708
|
+
* isActive: isActiveColumn,
|
|
709
|
+
* },
|
|
710
|
+
* });
|
|
711
|
+
*/
|
|
712
|
+
const column = {
|
|
713
|
+
int: () => new Column("int"),
|
|
714
|
+
float: () => new Column("float"),
|
|
715
|
+
number: () => new Column("number"),
|
|
716
|
+
bigint: () => new Column("bigint"),
|
|
717
|
+
text: () => new Column("text"),
|
|
718
|
+
string: () => new Column("string"),
|
|
719
|
+
char: (l) => new Column(`char(${isPositiveInteger(l) ? l : "0"})`),
|
|
720
|
+
varchar: (l) => new Column(`varchar(${isPositiveInteger(l) ? l : "0"})`),
|
|
721
|
+
uuid: () => new Column("uuid"),
|
|
722
|
+
timestamp: () => new Column("timestamp"),
|
|
723
|
+
bool: () => new Column("bool"),
|
|
724
|
+
date: () => new Column("date"),
|
|
725
|
+
object: () => new Column(`object`),
|
|
726
|
+
array: () => new Column(`array`),
|
|
727
|
+
list: () => new Column(`list`),
|
|
728
|
+
tuple: () => new Column(`tuple`),
|
|
729
|
+
set: () => new Column(`set`),
|
|
730
|
+
map: () => new Column(`map`),
|
|
731
|
+
custom: () => new Column(`custom`)
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
//#endregion
|
|
735
|
+
exports.Locality = Locality;
|
|
736
|
+
exports.column = column;
|
|
737
|
+
exports.defineSchema = defineSchema;
|
|
738
|
+
exports.getTimestamp = getTimestamp;
|
|
739
|
+
exports.openDBWithStores = openDBWithStores;
|
|
740
|
+
exports.table = table;
|
|
741
|
+
exports.uuidV4 = uuidV4;
|