locality-idb 1.1.1 → 1.2.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 +304 -27
- package/dist/index.cjs +94 -37
- package/dist/index.d.cts +309 -32
- package/dist/index.d.mts +309 -32
- package/dist/index.iife.js +94 -37
- package/dist/index.mjs +94 -38
- package/dist/index.umd.js +94 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -196,26 +196,182 @@ const multiPkSchema = defineSchema({
|
|
|
196
196
|
|
|
197
197
|
Locality IDB supports a wide range of column types:
|
|
198
198
|
|
|
199
|
-
| Type | Description | Example
|
|
200
|
-
| ---------------------- | ---------------------------------------------------------- |
|
|
201
|
-
| `number()` / `float()` | Numeric values (integer or float) | `column.
|
|
202
|
-
| `int()` | Numeric values (only integer is allowed) | `column.int()`
|
|
203
|
-
| `numeric()` | Number or numeric string | `column.numeric()`
|
|
204
|
-
| `bigint()` | Large integers | `column.bigint()`
|
|
205
|
-
| `text()` / `string()` | Text strings | `column.text()`
|
|
206
|
-
| `char(length?)` | Fixed-length string | `column.char(10)`
|
|
207
|
-
| `varchar(length?)` | Variable-length string | `column.varchar(255)`
|
|
208
|
-
| `bool()` / `boolean()` | Boolean values | `column.bool()`
|
|
209
|
-
| `date()` | Date objects | `column.date()`
|
|
210
|
-
| `timestamp()` | ISO 8601 timestamps ([auto-generated](#utility-functions)) | `column.timestamp()`
|
|
211
|
-
| `uuid()` | UUID strings ([auto-generated](#utility-functions) v4) | `column.uuid()`
|
|
212
|
-
| `object<T>()` | Generic objects | `column.object<UserData>()`
|
|
213
|
-
| `array<T>()` | Arrays | `column.array<number>()`
|
|
214
|
-
| `list<T>()` | Read-only arrays | `column.list<string>()`
|
|
215
|
-
| `tuple<T>()` | Fixed-size tuples | `column.tuple<
|
|
216
|
-
| `set<T>()` | Sets | `column.set<string>()`
|
|
217
|
-
| `map<K,V>()` | Maps | `column.map<string, number>()`
|
|
218
|
-
| `custom<T>()` | Custom types | `column.custom<MyType>()`
|
|
199
|
+
| Type | Description | Example |
|
|
200
|
+
| ---------------------- | ---------------------------------------------------------- | -------------------------------- |
|
|
201
|
+
| `number()` / `float()` | Numeric values (integer or float) | `column.number()` |
|
|
202
|
+
| `int()` | Numeric values (only integer is allowed) | `column.int()` |
|
|
203
|
+
| `numeric()` | Number or numeric string | `column.numeric()` |
|
|
204
|
+
| `bigint()` | Large integers | `column.bigint()` |
|
|
205
|
+
| `text()` / `string()` | Text strings | `column.text()` |
|
|
206
|
+
| `char(length?)` | Fixed-length string | `column.char(10)` |
|
|
207
|
+
| `varchar(length?)` | Variable-length string | `column.varchar(255)` |
|
|
208
|
+
| `bool()` / `boolean()` | Boolean values | `column.bool()` |
|
|
209
|
+
| `date()` | Date objects | `column.date()` |
|
|
210
|
+
| `timestamp()` | ISO 8601 timestamps ([auto-generated](#utility-functions)) | `column.timestamp()` |
|
|
211
|
+
| `uuid()` | UUID strings ([auto-generated](#utility-functions) v4) | `column.uuid()` |
|
|
212
|
+
| `object<T>()` | Generic objects | `column.object<UserData>()` |
|
|
213
|
+
| `array<T>()` | Arrays | `column.array<number>()` |
|
|
214
|
+
| `list<T>()` | Read-only arrays | `column.list<string>()` |
|
|
215
|
+
| `tuple<T>()` | Fixed-size tuples | `column.tuple<string, number>()` |
|
|
216
|
+
| `set<T>()` | Sets | `column.set<string>()` |
|
|
217
|
+
| `map<K,V>()` | Maps | `column.map<string, number>()` |
|
|
218
|
+
| `custom<T>()` | Custom types | `column.custom<MyType>()` |
|
|
219
|
+
|
|
220
|
+
#### Type Extensions
|
|
221
|
+
|
|
222
|
+
Most column types support **generic type parameters** for creating branded types, literal unions, or domain-specific types:
|
|
223
|
+
|
|
224
|
+
##### Numeric Types (`int`, `float`, `number`)
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// Basic usage
|
|
228
|
+
const age = column.int();
|
|
229
|
+
const price = column.float();
|
|
230
|
+
const score = column.number();
|
|
231
|
+
|
|
232
|
+
// Branded types for type safety
|
|
233
|
+
type UserId = Branded<number, 'UserId'>;
|
|
234
|
+
type ProductId = Branded<number, 'ProductId'>;
|
|
235
|
+
|
|
236
|
+
const schema = defineSchema({
|
|
237
|
+
users: {
|
|
238
|
+
id: column.int<UserId>().pk().auto(),
|
|
239
|
+
age: column.int(),
|
|
240
|
+
},
|
|
241
|
+
products: {
|
|
242
|
+
id: column.int<ProductId>().pk().auto(),
|
|
243
|
+
userId: column.int<UserId>(), // Type-safe foreign key
|
|
244
|
+
price: column.float(),
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ✅ Type safety prevents mixing IDs
|
|
249
|
+
const userId: UserId = 1 as UserId;
|
|
250
|
+
const productId: ProductId = 2 as ProductId;
|
|
251
|
+
// userId = productId; // ❌ Type error!
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
##### String Types (`text`, `string`, `char`, `varchar`)
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// Literal unions for enum-like behavior
|
|
258
|
+
type Role = 'admin' | 'user' | 'guest';
|
|
259
|
+
type Status = 'draft' | 'published' | 'archived';
|
|
260
|
+
|
|
261
|
+
const schema = defineSchema({
|
|
262
|
+
users: {
|
|
263
|
+
id: column.int().pk().auto(),
|
|
264
|
+
role: column.text<Role>().default('user'),
|
|
265
|
+
status: column.string<Status>().default('draft'),
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Branded types for domain-specific strings
|
|
270
|
+
type Email = Branded<string, 'Email'>;
|
|
271
|
+
type URL = Branded<string, 'URL'>;
|
|
272
|
+
|
|
273
|
+
const profileSchema = defineSchema({
|
|
274
|
+
profiles: {
|
|
275
|
+
id: column.int().pk().auto(),
|
|
276
|
+
email: column.varchar<Email>(255).unique(),
|
|
277
|
+
website: column.varchar<URL>(500).optional(),
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
##### Boolean Types (`bool`, `boolean`)
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// Branded booleans for clarity
|
|
286
|
+
type EmailVerified = Branded<boolean, 'EmailVerified'>;
|
|
287
|
+
type TwoFactorEnabled = Branded<boolean, 'TwoFactorEnabled'>;
|
|
288
|
+
|
|
289
|
+
const schema = defineSchema({
|
|
290
|
+
users: {
|
|
291
|
+
id: column.int().pk().auto(),
|
|
292
|
+
emailVerified: column.bool<EmailVerified>().default(false as EmailVerified),
|
|
293
|
+
twoFactorEnabled: column.boolean<TwoFactorEnabled>().default(false as TwoFactorEnabled),
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
##### Complex Types (`object`, `array`, `list`, `tuple`, `set`, `map`)
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// Object with typed structure
|
|
302
|
+
interface UserProfile {
|
|
303
|
+
avatar: string;
|
|
304
|
+
bio: string;
|
|
305
|
+
socials: {
|
|
306
|
+
twitter?: string;
|
|
307
|
+
github?: string;
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Array of typed elements
|
|
312
|
+
interface Comment {
|
|
313
|
+
author: string;
|
|
314
|
+
text: string;
|
|
315
|
+
date: string;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Map with typed keys and values
|
|
319
|
+
interface CacheEntry {
|
|
320
|
+
value: any;
|
|
321
|
+
expires: number;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const schema = defineSchema({
|
|
325
|
+
users: {
|
|
326
|
+
id: column.int().pk().auto(),
|
|
327
|
+
profile: column.object<UserProfile>(),
|
|
328
|
+
tags: column.array<string>(),
|
|
329
|
+
comments: column.array<Comment>(),
|
|
330
|
+
permissions: column.set<'read' | 'write' | 'delete'>(),
|
|
331
|
+
cache: column.map<string, CacheEntry>(),
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
// Tuples for fixed structures
|
|
335
|
+
locations: {
|
|
336
|
+
id: column.int().pk().auto(),
|
|
337
|
+
coordinates: column.tuple<number, number>(), // [latitude, longitude]
|
|
338
|
+
rgbColor: column.tuple<number, number, number>(), // [r, g, b]
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
// List (readonly array)
|
|
342
|
+
config: {
|
|
343
|
+
id: column.int().pk().auto(),
|
|
344
|
+
allowedOrigins: column.list<string>(), // Immutable at type level
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
##### Numeric & Bigint with `Numeric` & `bigint` Types
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
// Numeric accepts both number and numeric strings
|
|
353
|
+
const schema = defineSchema({
|
|
354
|
+
products: {
|
|
355
|
+
id: column.int().pk().auto(),
|
|
356
|
+
serialNumber: column.numeric(), // Can be 123 or "123"
|
|
357
|
+
largeId: column.bigint(), // For very large integers
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Branded Numeric types
|
|
362
|
+
type SerialNumber = Branded<Numeric, 'SerialNumber'>;
|
|
363
|
+
type SnowflakeId = Branded<bigint, 'SnowflakeId'>;
|
|
364
|
+
|
|
365
|
+
const advancedSchema = defineSchema({
|
|
366
|
+
items: {
|
|
367
|
+
id: column.int().pk().auto(),
|
|
368
|
+
serial: column.numeric<SerialNumber>(),
|
|
369
|
+
snowflake: column.bigint<SnowflakeId>(),
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
> **Note:** Type extensions are compile-time only and do not affect runtime validation. Use [custom validators](#validatevalidator-value-t--string--null--undefined-column) for runtime type enforcement.
|
|
219
375
|
|
|
220
376
|
### Type Inference
|
|
221
377
|
|
|
@@ -340,6 +496,7 @@ const allUsers = await db.from('users').findAll();
|
|
|
340
496
|
#### Filter with Where
|
|
341
497
|
|
|
342
498
|
```typescript
|
|
499
|
+
// Predicate-based filtering (in-memory)
|
|
343
500
|
const admins = await db
|
|
344
501
|
.from('users')
|
|
345
502
|
.where((user) => user.role === 'admin')
|
|
@@ -349,6 +506,18 @@ const activeUsers = await db
|
|
|
349
506
|
.from('users')
|
|
350
507
|
.where((user) => user.isActive && user.age >= 18)
|
|
351
508
|
.findAll();
|
|
509
|
+
|
|
510
|
+
// Index-based filtering (optimized) - requires index or primary key
|
|
511
|
+
const usersByEmail = await db
|
|
512
|
+
.from('users')
|
|
513
|
+
.where('email', 'alice@example.com')
|
|
514
|
+
.findAll();
|
|
515
|
+
|
|
516
|
+
// Range queries with IDBKeyRange
|
|
517
|
+
const adults = await db
|
|
518
|
+
.from('users')
|
|
519
|
+
.where('age', IDBKeyRange.bound(18, 65))
|
|
520
|
+
.findAll();
|
|
352
521
|
```
|
|
353
522
|
|
|
354
523
|
#### Select Specific Columns
|
|
@@ -627,7 +796,7 @@ Gets the underlying `IDBDatabase` instance.
|
|
|
627
796
|
const idb = await db.getDBInstance();
|
|
628
797
|
```
|
|
629
798
|
|
|
630
|
-
#### `seed<T>(table: T, data: InferInsertType<Schema[T]>
|
|
799
|
+
#### `seed<T>(table: T, data: InferInsertType<Schema[T]>[]): Promise<InferSelectType<Schema[T]>[]>`
|
|
631
800
|
|
|
632
801
|
Inserts seed data into the specified table.
|
|
633
802
|
|
|
@@ -636,13 +805,14 @@ Inserts seed data into the specified table.
|
|
|
636
805
|
> - This is a convenience method for inserting initial data.
|
|
637
806
|
> - It uses the `insert` method internally.
|
|
638
807
|
> - It does not clear existing data before inserting.
|
|
808
|
+
> - Accepts only an **array** of records (for single record insertion, use `insert().values().run()`)
|
|
639
809
|
|
|
640
810
|
**Parameters:**
|
|
641
811
|
|
|
642
812
|
- `table`: Table name
|
|
643
|
-
- `data`:
|
|
813
|
+
- `data`: Array of records to insert
|
|
644
814
|
|
|
645
|
-
**Returns:**
|
|
815
|
+
**Returns:** Array of inserted record(s)
|
|
646
816
|
|
|
647
817
|
**Example:**
|
|
648
818
|
|
|
@@ -761,6 +931,103 @@ column.bool().default(true)
|
|
|
761
931
|
column.text().default('N/A')
|
|
762
932
|
```
|
|
763
933
|
|
|
934
|
+
#### `validate(validator: (value: T) => string | null | undefined): Column`
|
|
935
|
+
|
|
936
|
+
Adds custom validation logic to the column. The validation function receives the column value and should return:
|
|
937
|
+
|
|
938
|
+
- `null` or `undefined` if the value is valid
|
|
939
|
+
- An error message `string` if the value is invalid
|
|
940
|
+
|
|
941
|
+
**When it runs:** During insert and update operations, before data is saved to `IndexedDB`.
|
|
942
|
+
|
|
943
|
+
> **Error Handling:** If validation fails, a `TypeError` is thrown with details about the invalid field.
|
|
944
|
+
|
|
945
|
+
**Precedence:** Custom validators override built-in type validation. If you provide a custom validator, the built-in type check for that column will be skipped.
|
|
946
|
+
|
|
947
|
+
> **Note:**
|
|
948
|
+
>
|
|
949
|
+
> - Custom validation is not applied to auto-generated values (e.g. auto-increment, UUID, timestamp). But default values are validated if `.default(value)` is used.
|
|
950
|
+
> - If multiple validators are chained, only the last one is used.
|
|
951
|
+
> - Built-in type validation still applies to all other columns without custom validators.
|
|
952
|
+
> - If the column is optional, the validator is only called when a value is provided (not `undefined`).
|
|
953
|
+
|
|
954
|
+
```typescript
|
|
955
|
+
// Email validation
|
|
956
|
+
const schema = defineSchema({
|
|
957
|
+
users: {
|
|
958
|
+
id: column.int().pk().auto(),
|
|
959
|
+
email: column.text().validate((val) => {
|
|
960
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
961
|
+
return emailRegex.test(val) ? null : 'Invalid email format';
|
|
962
|
+
}),
|
|
963
|
+
age: column.int().validate((val) => {
|
|
964
|
+
if (val < 0) return 'Age cannot be negative';
|
|
965
|
+
if (val > 120) return 'Age must be 120 or less';
|
|
966
|
+
return null; // Valid
|
|
967
|
+
}),
|
|
968
|
+
username: column.text().validate((val) => {
|
|
969
|
+
if (val.length < 3) return 'Username must be at least 3 characters';
|
|
970
|
+
if (!/^[a-zA-Z0-9_]+$/.test(val)) return 'Username can only contain letters, numbers, and underscores';
|
|
971
|
+
return null;
|
|
972
|
+
}),
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
// ✅ Valid insert
|
|
977
|
+
await db.insert('users').values({
|
|
978
|
+
email: 'user@example.com',
|
|
979
|
+
age: 25,
|
|
980
|
+
username: 'john_doe'
|
|
981
|
+
}).run();
|
|
982
|
+
|
|
983
|
+
// ❌ Throws TypeError: Invalid value for field 'email' in table 'users': Invalid email format
|
|
984
|
+
await db.insert('users').values({
|
|
985
|
+
email: 'invalid-email',
|
|
986
|
+
age: 25,
|
|
987
|
+
username: 'john_doe'
|
|
988
|
+
}).run();
|
|
989
|
+
|
|
990
|
+
// ❌ Throws TypeError: Invalid value for field 'age' in table 'users': Age cannot be negative
|
|
991
|
+
await db.insert('users').values({
|
|
992
|
+
email: 'user@example.com',
|
|
993
|
+
age: -5,
|
|
994
|
+
username: 'john_doe'
|
|
995
|
+
}).run();
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
**Combining with `.optional()`:**
|
|
999
|
+
|
|
1000
|
+
```typescript
|
|
1001
|
+
const schema = defineSchema({
|
|
1002
|
+
users: {
|
|
1003
|
+
id: column.int().pk().auto(),
|
|
1004
|
+
// Custom validation only runs when value is provided
|
|
1005
|
+
bio: column.text().optional().validate((val) => {
|
|
1006
|
+
return val.length <= 500 ? null : 'Bio must be 500 characters or less';
|
|
1007
|
+
}),
|
|
1008
|
+
},
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
// ✅ Valid - bio is optional and omitted
|
|
1012
|
+
await db.insert('users').values({}).run();
|
|
1013
|
+
|
|
1014
|
+
// ✅ Valid - bio is provided and valid
|
|
1015
|
+
await db.insert('users').values({ bio: 'Short bio' }).run();
|
|
1016
|
+
|
|
1017
|
+
// ❌ Throws TypeError - bio provided but exceeds 500 chars
|
|
1018
|
+
await db.insert('users').values({ bio: 'x'.repeat(501) }).run();
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
**Access the `ValidateFn` symbol (advanced):**
|
|
1022
|
+
|
|
1023
|
+
```typescript
|
|
1024
|
+
import { ValidateFn } from 'locality-idb';
|
|
1025
|
+
|
|
1026
|
+
// Access validator function programmatically
|
|
1027
|
+
const emailColumn = column.text().validate((val) => { /* ... */ });
|
|
1028
|
+
const validatorFn = emailColumn[ValidateFn]; // Function reference
|
|
1029
|
+
```
|
|
1030
|
+
|
|
764
1031
|
---
|
|
765
1032
|
|
|
766
1033
|
### Query Methods
|
|
@@ -789,17 +1056,25 @@ db.from('users').where((user) => user.age >= 18)
|
|
|
789
1056
|
|
|
790
1057
|
##### `where<IdxKey>(indexName: IdxKey, query: T[IdxKey] | IDBKeyRange): SelectQuery`
|
|
791
1058
|
|
|
792
|
-
Filters rows using an indexed field.
|
|
1059
|
+
Filters rows using an indexed field or primary key.
|
|
1060
|
+
|
|
1061
|
+
**Type Safety:** `indexName` must be either an indexed field or the primary key.
|
|
1062
|
+
|
|
1063
|
+
**Performance:** Uses IndexedDB's optimized index/key query for efficient lookups.
|
|
793
1064
|
|
|
794
1065
|
```typescript
|
|
1066
|
+
// Using an indexed field
|
|
795
1067
|
db.from('users').where('age', IDBKeyRange.bound(18, 30))
|
|
1068
|
+
|
|
1069
|
+
// Using primary key
|
|
1070
|
+
db.from('users').where('id', IDBKeyRange.bound(1, 100))
|
|
796
1071
|
```
|
|
797
1072
|
|
|
798
1073
|
##### `sortByIndex<IdxKey>(indexName: IdxKey, dir?: 'asc' | 'desc'): SelectQuery`
|
|
799
1074
|
|
|
800
1075
|
Sorts results by an indexed field using IndexedDB cursor iteration (avoiding in-memory sorting).
|
|
801
1076
|
|
|
802
|
-
**Type Safety:** `indexName` must be a field with an index.
|
|
1077
|
+
**Type Safety:** `indexName` must be a field with an index or the primary key.
|
|
803
1078
|
|
|
804
1079
|
**Performance:** Uses IndexedDB's cursor for optimized sorting. For large datasets, this is significantly more efficient than in-memory sorting.
|
|
805
1080
|
|
|
@@ -890,8 +1165,10 @@ const userCount = await db.from('users').where((user) => user.isActive).count()
|
|
|
890
1165
|
|
|
891
1166
|
> **Note:**
|
|
892
1167
|
>
|
|
893
|
-
> -
|
|
894
|
-
>
|
|
1168
|
+
> - Uses IndexedDB's optimized `count()` when:
|
|
1169
|
+
> - No `where()` clause is applied, OR
|
|
1170
|
+
> - `where()` uses an index or primary key
|
|
1171
|
+
> - Falls back to in-memory counting when `where()` uses a predicate function
|
|
895
1172
|
|
|
896
1173
|
##### `exists(): Promise<boolean>`
|
|
897
1174
|
|
package/dist/index.cjs
CHANGED
|
@@ -153,6 +153,8 @@ const IsIndexed = Symbol("IsIndexed");
|
|
|
153
153
|
const IsUnique = Symbol("IsUnique");
|
|
154
154
|
/** Symbol key for default value */
|
|
155
155
|
const DefaultValue = Symbol("DefaultValue");
|
|
156
|
+
/** Symbol key for custom validation function */
|
|
157
|
+
const ValidateFn = Symbol("ValidateFn");
|
|
156
158
|
/** @class Represents a column definition. */
|
|
157
159
|
var Column = class {
|
|
158
160
|
constructor(type) {
|
|
@@ -200,6 +202,34 @@ var Column = class {
|
|
|
200
202
|
this[IsOptional] = true;
|
|
201
203
|
return this;
|
|
202
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* @instance Sets a custom validation function for the column
|
|
207
|
+
*
|
|
208
|
+
* @param validator - Custom validation function that receives the value and returns `null`/`undefined` if valid, or an error message `string` if invalid
|
|
209
|
+
*
|
|
210
|
+
* @returns The column instance with the validation function attached
|
|
211
|
+
*
|
|
212
|
+
* @remarks
|
|
213
|
+
* - Custom validation is not applied to auto-generated values (e.g. auto-increment, UUID, timestamp). But default values are validated if {@link default()} is used.
|
|
214
|
+
* - If multiple validators are chained, only the last one is used.
|
|
215
|
+
* - Built-in type validation still applies to all other columns without custom validators.
|
|
216
|
+
* - If the column is optional, the validator is only called when a value is provided (not `undefined`).
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* // Email validation
|
|
220
|
+
* email: text().validate((val) => {
|
|
221
|
+
* return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val) ? null : 'Invalid email format';
|
|
222
|
+
* })
|
|
223
|
+
*
|
|
224
|
+
* // Range validation
|
|
225
|
+
* age: int().validate((val) => {
|
|
226
|
+
* return val >= 0 && val <= 120 ? null : 'Age must be between 0 and 120';
|
|
227
|
+
* })
|
|
228
|
+
*/
|
|
229
|
+
validate(validator) {
|
|
230
|
+
this[ValidateFn] = validator;
|
|
231
|
+
return this;
|
|
232
|
+
}
|
|
203
233
|
};
|
|
204
234
|
/** @class Represents a table. */
|
|
205
235
|
var Table = class {
|
|
@@ -381,15 +411,15 @@ function validateColumnType(type, value) {
|
|
|
381
411
|
case "tuple":
|
|
382
412
|
if (isArray(value)) return null;
|
|
383
413
|
return `${strVal} is not a tuple`;
|
|
384
|
-
case "set":
|
|
385
|
-
if (isSet(value)) return null;
|
|
386
|
-
return `${strVal} is not a set`;
|
|
387
414
|
case "object":
|
|
388
415
|
if (isObject(value)) return null;
|
|
389
416
|
return `${strVal} is not an object`;
|
|
390
417
|
case "date":
|
|
391
418
|
if (isDate(value)) return null;
|
|
392
419
|
return `${strVal} is not a Date object`;
|
|
420
|
+
case "set":
|
|
421
|
+
if (isSet(value)) return null;
|
|
422
|
+
return `${strVal} is not a set`;
|
|
393
423
|
case "map":
|
|
394
424
|
if (isMap(value)) return null;
|
|
395
425
|
return `${strVal} is not a Map object`;
|
|
@@ -426,22 +456,48 @@ function validateColumnType(type, value) {
|
|
|
426
456
|
* @returns The validated and prepared data object
|
|
427
457
|
* @throws
|
|
428
458
|
* - A {@link TypeError} if any value does not match the expected column type
|
|
429
|
-
* - A {@link RangeError} if any field is not defined in the table schema
|
|
459
|
+
* - A {@link RangeError} if any field is not defined in the table schema or required field is missing
|
|
430
460
|
*/
|
|
431
461
|
function validateAndPrepareData(data, columns, keyPath, tableName, forUpdate = false) {
|
|
432
462
|
const prepared = { ...data };
|
|
433
463
|
if (columns) {
|
|
434
|
-
for (const fieldName of Object.keys(prepared)) if (!Object.keys(columns).includes(fieldName)) throw new RangeError(`'${fieldName}' is not defined in the table
|
|
464
|
+
for (const fieldName of Object.keys(prepared)) if (!Object.keys(columns).includes(fieldName)) throw new RangeError(`Field '${fieldName}' is not defined in the table '${tableName}' schema!`);
|
|
435
465
|
Object.entries(columns).forEach((entry) => {
|
|
436
466
|
const [fieldName, column] = entry;
|
|
437
|
-
const defaultValue = column[DefaultValue];
|
|
438
|
-
if (!(fieldName in prepared) && defaultValue !== void 0 && !forUpdate) prepared[fieldName] = defaultValue;
|
|
439
467
|
const columnType = column[ColumnType];
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (
|
|
468
|
+
const defaultValue = column[DefaultValue];
|
|
469
|
+
const isOptional = column[IsOptional] ?? false;
|
|
470
|
+
let fieldNotPresent = !(fieldName in prepared);
|
|
471
|
+
if (!forUpdate && fieldNotPresent) {
|
|
472
|
+
if (columnType === "uuid" && isUndefined(defaultValue)) {
|
|
473
|
+
prepared[fieldName] = uuidV4();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (columnType === "timestamp" && isUndefined(defaultValue)) {
|
|
477
|
+
prepared[fieldName] = getTimestamp();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (!isUndefined(defaultValue)) {
|
|
481
|
+
prepared[fieldName] = defaultValue;
|
|
482
|
+
fieldNotPresent = false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const fieldValue = prepared[fieldName];
|
|
486
|
+
if (fieldNotPresent) {
|
|
487
|
+
if (forUpdate) return;
|
|
488
|
+
if (!isOptional && fieldName !== keyPath) throw new RangeError(`Required field '${String(fieldName)}' is missing in table '${tableName}'!`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (isUndefined(fieldValue)) {
|
|
492
|
+
if (!isOptional && fieldName !== keyPath) throw new TypeError(`Field '${String(fieldName)}' in table '${tableName}' cannot be undefined. It is a required field.`);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (!(!forUpdate && fieldName === keyPath && (column[IsAutoInc] ?? false))) {
|
|
496
|
+
const customValidator = column[ValidateFn];
|
|
497
|
+
let errorMsg;
|
|
498
|
+
if (isFunction(customValidator)) errorMsg = customValidator(fieldValue);
|
|
499
|
+
else errorMsg = validateColumnType(columnType, fieldValue);
|
|
500
|
+
if (errorMsg) throw new TypeError(`Invalid value for field '${String(fieldName)}' in table '${tableName}': ${errorMsg}`);
|
|
445
501
|
}
|
|
446
502
|
});
|
|
447
503
|
}
|
|
@@ -473,11 +529,13 @@ var SelectQuery = class {
|
|
|
473
529
|
this.#dbGetter = dbGetter;
|
|
474
530
|
this.#readyPromise = readyPromise;
|
|
475
531
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
532
|
+
/** @internal Create a readonly transaction and return the store */
|
|
533
|
+
#getStore() {
|
|
534
|
+
const transaction = this.#dbGetter().transaction(this.#table, "readonly");
|
|
535
|
+
return {
|
|
536
|
+
transaction,
|
|
537
|
+
store: transaction.objectStore(this.#table)
|
|
538
|
+
};
|
|
481
539
|
}
|
|
482
540
|
/** @internal Check if key is an index on the store for the `#whereIndexName` */
|
|
483
541
|
#isIndexKey(store) {
|
|
@@ -505,6 +563,12 @@ var SelectQuery = class {
|
|
|
505
563
|
});
|
|
506
564
|
return data;
|
|
507
565
|
}
|
|
566
|
+
/** @internal Apply sort, limit, and projection pipeline to results */
|
|
567
|
+
#applyPipeline(results) {
|
|
568
|
+
let processed = this.#sort(results);
|
|
569
|
+
if (this.#limitCount) processed = processed.slice(0, this.#limitCount);
|
|
570
|
+
return processed.map((row) => this.#projectRow(row));
|
|
571
|
+
}
|
|
508
572
|
/** Projects a row based on selected fields */
|
|
509
573
|
#projectRow(row) {
|
|
510
574
|
if (!isNotEmptyObject(this?.[Selected])) return row;
|
|
@@ -577,16 +641,13 @@ var SelectQuery = class {
|
|
|
577
641
|
async findAll() {
|
|
578
642
|
await this.#readyPromise;
|
|
579
643
|
return new Promise((resolve, reject) => {
|
|
580
|
-
const store = this.#
|
|
644
|
+
const { store } = this.#getStore();
|
|
581
645
|
if (this.#whereIndexName && !isUndefined(this.#whereIndexQuery)) {
|
|
582
646
|
const source = this.#buildIndexedStore(store, reject);
|
|
583
647
|
if (!source) return;
|
|
584
648
|
const request = source.getAll(this.#whereIndexQuery);
|
|
585
649
|
request.onsuccess = () => {
|
|
586
|
-
|
|
587
|
-
results = this.#sort(results);
|
|
588
|
-
if (this.#limitCount) results = results.slice(0, this.#limitCount);
|
|
589
|
-
resolve(results.map((row) => this.#projectRow(row)));
|
|
650
|
+
resolve(this.#applyPipeline(request.result));
|
|
590
651
|
};
|
|
591
652
|
request.onerror = () => reject(request.error);
|
|
592
653
|
return;
|
|
@@ -615,9 +676,7 @@ var SelectQuery = class {
|
|
|
615
676
|
request.onsuccess = () => {
|
|
616
677
|
let results = request.result;
|
|
617
678
|
if (this.#whereCondition) results = results.filter(this.#whereCondition);
|
|
618
|
-
|
|
619
|
-
if (this.#limitCount) results = results.slice(0, this.#limitCount);
|
|
620
|
-
resolve(results.map((row) => this.#projectRow(row)));
|
|
679
|
+
resolve(this.#applyPipeline(results));
|
|
621
680
|
};
|
|
622
681
|
request.onerror = () => reject(request.error);
|
|
623
682
|
}
|
|
@@ -626,15 +685,14 @@ var SelectQuery = class {
|
|
|
626
685
|
async findFirst() {
|
|
627
686
|
await this.#readyPromise;
|
|
628
687
|
return new Promise((resolve, reject) => {
|
|
629
|
-
const store = this.#
|
|
688
|
+
const { store } = this.#getStore();
|
|
630
689
|
if (this.#whereIndexName && !isUndefined(this.#whereIndexQuery)) {
|
|
631
690
|
const source = this.#buildIndexedStore(store, reject);
|
|
632
691
|
if (!source) return;
|
|
633
692
|
const request = source.getAll(this.#whereIndexQuery);
|
|
634
693
|
request.onsuccess = () => {
|
|
635
694
|
const results = request.result;
|
|
636
|
-
|
|
637
|
-
else resolve(null);
|
|
695
|
+
resolve(results.length > 0 ? this.#projectRow(results[0]) : null);
|
|
638
696
|
};
|
|
639
697
|
request.onerror = () => reject(request.error);
|
|
640
698
|
return;
|
|
@@ -643,8 +701,7 @@ var SelectQuery = class {
|
|
|
643
701
|
request.onsuccess = () => {
|
|
644
702
|
let results = request.result;
|
|
645
703
|
if (this.#whereCondition) results = results.filter(this.#whereCondition);
|
|
646
|
-
|
|
647
|
-
else resolve(null);
|
|
704
|
+
resolve(results.length > 0 ? this.#projectRow(results[0]) : null);
|
|
648
705
|
};
|
|
649
706
|
request.onerror = () => reject(request.error);
|
|
650
707
|
});
|
|
@@ -661,7 +718,8 @@ var SelectQuery = class {
|
|
|
661
718
|
async findByPk(key) {
|
|
662
719
|
await this.#readyPromise;
|
|
663
720
|
return new Promise((resolve, reject) => {
|
|
664
|
-
const
|
|
721
|
+
const { store } = this.#getStore();
|
|
722
|
+
const request = store.get(key);
|
|
665
723
|
request.onsuccess = () => {
|
|
666
724
|
const result = request.result;
|
|
667
725
|
if (!result) {
|
|
@@ -690,18 +748,16 @@ var SelectQuery = class {
|
|
|
690
748
|
async findByIndex(indexName, query) {
|
|
691
749
|
await this.#readyPromise;
|
|
692
750
|
return new Promise((resolve, reject) => {
|
|
693
|
-
const store = this.#
|
|
751
|
+
const { store } = this.#getStore();
|
|
694
752
|
if (!store.indexNames.contains(indexName)) {
|
|
695
|
-
reject(/* @__PURE__ */ new
|
|
753
|
+
reject(/* @__PURE__ */ new RangeError(`Index '${indexName}' does not exist on table '${this.#table}'`));
|
|
696
754
|
return;
|
|
697
755
|
}
|
|
698
756
|
const request = store.index(indexName).getAll(query);
|
|
699
757
|
request.onsuccess = () => {
|
|
700
758
|
let results = request.result;
|
|
701
759
|
if (this.#whereCondition) results = results.filter(this.#whereCondition);
|
|
702
|
-
|
|
703
|
-
if (this.#limitCount) results = results.slice(0, this.#limitCount);
|
|
704
|
-
resolve(results.map((row) => this.#projectRow(row)));
|
|
760
|
+
resolve(this.#applyPipeline(results));
|
|
705
761
|
};
|
|
706
762
|
request.onerror = () => reject(request.error);
|
|
707
763
|
});
|
|
@@ -710,7 +766,7 @@ var SelectQuery = class {
|
|
|
710
766
|
async count() {
|
|
711
767
|
await this.#readyPromise;
|
|
712
768
|
return new Promise((resolve, reject) => {
|
|
713
|
-
const store = this.#
|
|
769
|
+
const { store } = this.#getStore();
|
|
714
770
|
if (this.#whereIndexName && !isUndefined(this.#whereIndexQuery)) {
|
|
715
771
|
const source = this.#buildIndexedStore(store, reject);
|
|
716
772
|
if (!source) return;
|
|
@@ -1224,6 +1280,7 @@ const column = {
|
|
|
1224
1280
|
|
|
1225
1281
|
//#endregion
|
|
1226
1282
|
exports.Locality = Locality;
|
|
1283
|
+
exports.ValidateFn = ValidateFn;
|
|
1227
1284
|
exports.column = column;
|
|
1228
1285
|
exports.defineSchema = defineSchema;
|
|
1229
1286
|
exports.deleteDB = deleteDB;
|