locality-idb 1.2.0 → 1.3.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 CHANGED
@@ -16,14 +16,14 @@
16
16
 
17
17
  [API Reference](#-api-reference) • [Examples](#-usage) • [Contributing](CONTRIBUTING.md)
18
18
 
19
- </div>
20
-
21
19
  ---
22
20
 
23
21
  ## Why Locality IDB?
24
22
 
25
23
  [**IndexedDB**](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB) is a powerful browser-native database, but its low-level API can be cumbersome and complex to work with. `Locality IDB` simplifies `IndexedDB` interactions by providing a modern, type-safe, and SQL-like query builder inspired by [**Drizzle ORM**](https://github.com/drizzle-team/drizzle-orm).
26
24
 
25
+ </div>
26
+
27
27
  ---
28
28
 
29
29
  ## 📋 Table of Contents
@@ -41,6 +41,8 @@
41
41
  - [Select/Query Records](#selectquery-records)
42
42
  - [Update Records](#update-records)
43
43
  - [Delete Records](#delete-records)
44
+ - [Transactions](#transactions)
45
+ - [Export Database](#export-database)
44
46
  - [API Reference](#-api-reference)
45
47
  - [Locality Class](#locality-class)
46
48
  - [Schema Functions](#schema-functions)
@@ -61,9 +63,11 @@
61
63
  - 📦 **Zero Dependencies**: Lightweight with only development dependencies
62
64
  - 🔄 **Auto-Generation**: Automatic UUID and timestamp generation
63
65
  - 🎨 **Schema-First**: Define your database schema with a simple, declarative API
64
- - ⚡ **Promise-Based**: Fully async/await compatible
65
66
  - 🛠️ **Rich Column Types**: Support for various data types including custom types
66
67
  - ✅ **Built-in Validation**: Automatic data type validation for built-in column types during insert and update operations
68
+ - 🔧 **Custom Validators**: Define custom validation logic for columns to enforce complex rules
69
+ - 🔒 **Atomic Transactions**: Execute multiple operations across tables with automatic rollback on failure
70
+ - 📤 **Database Export**: Export database data as JSON for backup, migration, or debugging
67
71
 
68
72
  ---
69
73
 
@@ -205,6 +209,8 @@ Locality IDB supports a wide range of column types:
205
209
  | `text()` / `string()` | Text strings | `column.text()` |
206
210
  | `char(length?)` | Fixed-length string | `column.char(10)` |
207
211
  | `varchar(length?)` | Variable-length string | `column.varchar(255)` |
212
+ | `email()` | Email strings | `column.email()` |
213
+ | `url()` | URL strings | `column.url()` |
208
214
  | `bool()` / `boolean()` | Boolean values | `column.bool()` |
209
215
  | `date()` | Date objects | `column.date()` |
210
216
  | `timestamp()` | ISO 8601 timestamps ([auto-generated](#utility-functions)) | `column.timestamp()` |
@@ -251,7 +257,7 @@ const productId: ProductId = 2 as ProductId;
251
257
  // userId = productId; // ❌ Type error!
252
258
  ```
253
259
 
254
- ##### String Types (`text`, `string`, `char`, `varchar`)
260
+ ##### String Types (`text`, `string`, `char`, `varchar`, `email`, `url`)
255
261
 
256
262
  ```typescript
257
263
  // Literal unions for enum-like behavior
@@ -277,8 +283,40 @@ const profileSchema = defineSchema({
277
283
  website: column.varchar<URL>(500).optional(),
278
284
  },
279
285
  });
286
+
287
+ // or use specialized `email` & `url` methods + types with built-in validation
288
+ const advancedProfileSchema = defineSchema({
289
+ profiles: {
290
+ id: column.int().pk().auto(),
291
+ email: column.email().unique(), // Validates emails
292
+ website: column.url().optional(), // Validates URLs (internally uses URL constructor)
293
+ },
294
+ });
295
+ ```
296
+
297
+ #### Auto-generated Types (`uuid`, `timestamp`)
298
+
299
+ ```typescript
300
+ const schema = defineSchema({
301
+ sessions: {
302
+ id: column.uuid().pk(), // Auto-generated UUID v4
303
+ idWithDefault: column.uuid().pk().default(uuid({ version: 'v6' })), // Replace auto-generated UUID v4
304
+ createdAt: column.timestamp(), // Auto-generated timestamp
305
+ defaultTs: column.timestamp().default(getTimestamp()), // Auto-generated timestamp with default using utility built-in function
306
+ customTs: column.timestamp().default(new Chronos().toLocalISOString() as Timestamp), // Default timestamp with custom format
307
+ },
308
+ });
280
309
  ```
281
310
 
311
+ > **Note:**
312
+ >
313
+ > - Auto-generated values can be overridden by providing explicit values during insert.
314
+ > - Use the `default()` modifier to set custom default values instead of auto-generated ones.
315
+ > - Auto-generated values are generated at runtime during insert operations.
316
+ > - Type extensions for `uuid` and `timestamp` are not applicable since they are already typed.
317
+ > - For custom UUID versions, use [`uuid`](https://toolbox.nazmul-nhb.dev/docs/utilities/hash/uuid) utility from [`nhb-toolbox`](https://www.npmjs.com/package/nhb-toolbox).
318
+ > - For custom timestamp formats, use date libraries like [`Chronos`](https://toolbox.nazmul-nhb.dev/docs/classes/Chronos) (from [`nhb-toolbox`](https://www.npmjs.com/package/nhb-toolbox)) or [`date-fns`](https://www.npmjs.com/package/date-fns) to generate ISO 8601 strings.
319
+
282
320
  ##### Boolean Types (`bool`, `boolean`)
283
321
 
284
322
  ```typescript
@@ -688,6 +726,163 @@ await db
688
726
  .run();
689
727
  ```
690
728
 
729
+ ### Transactions
730
+
731
+ Transactions enable you to perform multiple operations across multiple tables atomically. All operations in a transaction either succeed together or fail together, ensuring data consistency.
732
+
733
+ #### Basic Transaction
734
+
735
+ ```typescript
736
+ // Create a user and their first post atomically
737
+ await db.transaction(['users', 'posts'], async (ctx) => {
738
+ const newUser = await ctx
739
+ .insert('users')
740
+ .values({ name: 'John Doe', email: 'john@example.com' })
741
+ .run();
742
+
743
+ await ctx
744
+ .insert('posts')
745
+ .values({
746
+ userId: newUser.id,
747
+ title: 'My First Post',
748
+ content: 'Hello World!',
749
+ })
750
+ .run();
751
+ });
752
+ ```
753
+
754
+ #### Transaction with Multiple Operations
755
+
756
+ ```typescript
757
+ // Transfer data between tables atomically
758
+ await db.transaction(['users', 'posts', 'comments'], async (ctx) => {
759
+ // Update user
760
+ await ctx
761
+ .update('users')
762
+ .set({ isActive: true })
763
+ .where((user) => user.id === 1)
764
+ .run();
765
+
766
+ // Create post
767
+ const post = await ctx
768
+ .insert('posts')
769
+ .values({ userId: 1, title: 'New Post', content: 'Content' })
770
+ .run();
771
+
772
+ // Add comment
773
+ await ctx
774
+ .insert('comments')
775
+ .values({ postId: post.id, userId: 1, text: 'First comment!' })
776
+ .run();
777
+
778
+ // Query within transaction
779
+ const userPosts = await ctx
780
+ .from('posts')
781
+ .where((p) => p.userId === 1)
782
+ .findAll();
783
+
784
+ console.log(`User now has ${userPosts.length} posts`);
785
+ });
786
+ ```
787
+
788
+ #### Automatic Rollback
789
+
790
+ ```typescript
791
+ try {
792
+ await db.transaction(['users', 'posts'], async (ctx) => {
793
+ const user = await ctx
794
+ .insert('users')
795
+ .values({ name: 'Alice', email: 'alice@example.com' })
796
+ .run();
797
+
798
+ // This will cause the entire transaction to rollback
799
+ throw new Error('Something went wrong!');
800
+
801
+ // This never executes
802
+ await ctx
803
+ .insert('posts')
804
+ .values({ userId: user.id, title: 'Post' })
805
+ .run();
806
+ });
807
+ } catch (error) {
808
+ console.error('Transaction failed:', error);
809
+ // No data was inserted - transaction was rolled back
810
+ }
811
+ ```
812
+
813
+ > **Note:**
814
+ >
815
+ > - Transactions guarantee atomicity: all operations succeed or all fail.
816
+ > - If any operation fails or an error is thrown, the entire transaction is automatically rolled back.
817
+ > - Transaction context (`ctx`) provides `insert()`, `update()`, `delete()`, and `from()` methods.
818
+ > - All operations must be performed on tables specified in the transaction.
819
+
820
+ ### Export Database
821
+
822
+ Export your database data as JSON for backup, migration, or debugging purposes. The export includes metadata and table data, and automatically triggers a browser download.
823
+
824
+ #### Export All Tables
825
+
826
+ ```typescript
827
+ // Export entire database with pretty-printed JSON
828
+ await db.export();
829
+ // Downloads: my-database-2026-02-04T10-30-45-123Z.json
830
+ ```
831
+
832
+ #### Export Specific Tables
833
+
834
+ ```typescript
835
+ // Export only users and posts tables
836
+ await db.export({
837
+ tables: ['users', 'posts'],
838
+ filename: 'users-posts-backup.json',
839
+ });
840
+ ```
841
+
842
+ #### Export with Custom Options
843
+
844
+ ```typescript
845
+ // Export with custom configuration
846
+ await db.export({
847
+ tables: ['users'], // Optional: specific tables
848
+ filename: 'users-export.json', // Optional: custom filename
849
+ pretty: false, // Optional: compact JSON (default: true)
850
+ includeMetadata: true, // Optional: include metadata (default: true)
851
+ });
852
+ ```
853
+
854
+ #### Export Data Structure
855
+
856
+ The exported JSON file contains:
857
+
858
+ ```json
859
+ {
860
+ "metadata": {
861
+ "dbName": "my-database",
862
+ "version": 1,
863
+ "exportedAt": "2026-02-04T10:30:45.123Z",
864
+ "tables": ["users", "posts"]
865
+ },
866
+ "data": {
867
+ "users": [
868
+ { "id": 1, "name": "Alice", "email": "alice@example.com" },
869
+ { "id": 2, "name": "Bob", "email": "bob@example.com" }
870
+ ],
871
+ "posts": [
872
+ { "id": 1, "userId": 1, "title": "First Post", "content": "..." }
873
+ ]
874
+ }
875
+ }
876
+ ```
877
+
878
+ > **Note:**
879
+ >
880
+ > - Exported files are automatically downloaded in the browser.
881
+ > - Default filename format: `{dbName}-{timestamp}.json`
882
+ > - Metadata is included by default but can be disabled.
883
+ > - Use `pretty: true` (default) for human-readable JSON.
884
+ > - Use `pretty: false` for compact JSON (smaller file size).
885
+
691
886
  ---
692
887
 
693
888
  ## 📚 API Reference
@@ -827,6 +1022,105 @@ const allUsers = await db.from('users').findAll();
827
1022
  console.log(allUsers);
828
1023
  ```
829
1024
 
1025
+ #### `transaction<Tables>(tables: Tables[], callback: TransactionCallback): Promise<void>`
1026
+
1027
+ Executes multiple database operations across multiple tables in a single atomic transaction.
1028
+
1029
+ **Parameters:**
1030
+
1031
+ - `tables`: Array of table names to include in the transaction
1032
+ - `callback`: Async function that receives a transaction context and performs operations
1033
+
1034
+ **Returns:** Promise that resolves when the transaction completes successfully
1035
+
1036
+ **Transaction Context Methods:**
1037
+
1038
+ - `ctx.insert(table)`: Insert records within the transaction
1039
+ - `ctx.update(table)`: Update records within the transaction
1040
+ - `ctx.delete(table)`: Delete records within the transaction
1041
+ - `ctx.from(table)`: Query records within the transaction
1042
+
1043
+ **Example:**
1044
+
1045
+ ```typescript
1046
+ // Create user and post atomically
1047
+ await db.transaction(['users', 'posts'], async (ctx) => {
1048
+ const user = await ctx
1049
+ .insert('users')
1050
+ .values({ name: 'Alice', email: 'alice@example.com' })
1051
+ .run();
1052
+
1053
+ await ctx
1054
+ .insert('posts')
1055
+ .values({ userId: user.id, title: 'First Post', content: 'Hello!' })
1056
+ .run();
1057
+ });
1058
+ ```
1059
+
1060
+ > **Important:**
1061
+ >
1062
+ > - All operations succeed or all fail (atomicity).
1063
+ > - If any operation fails or an error is thrown, the entire transaction is rolled back.
1064
+ > - Only tables specified in the `tables` array can be accessed within the transaction.
1065
+ > - Transactions use IndexedDB's native transaction mechanism.
1066
+
1067
+ #### `export(options?: ExportOptions): Promise<void>`
1068
+
1069
+ Exports database data as a JSON file and triggers a browser download.
1070
+
1071
+ **Parameters:**
1072
+
1073
+ - `options`: Optional export configuration
1074
+ - `options.tables`: Array of table names to export (default: all tables)
1075
+ - `options.filename`: Custom filename (default: `{dbName}-{timestamp}.json`)
1076
+ - `options.pretty`: Enable pretty-printed JSON (default: `true`)
1077
+ - `options.includeMetadata`: Include export metadata (default: `true`)
1078
+
1079
+ **Returns:** Promise that resolves when the export completes
1080
+
1081
+ **Example:**
1082
+
1083
+ ```typescript
1084
+ // Export all tables with default settings
1085
+ await db.export();
1086
+
1087
+ // Export specific tables with custom filename
1088
+ await db.export({
1089
+ tables: ['users', 'posts'],
1090
+ filename: 'backup-2026-02-04.json',
1091
+ pretty: true,
1092
+ });
1093
+
1094
+ // Export without metadata in compact format
1095
+ await db.export({
1096
+ pretty: false,
1097
+ includeMetadata: false,
1098
+ });
1099
+ ```
1100
+
1101
+ **Exported JSON Structure:**
1102
+
1103
+ ```typescript
1104
+ {
1105
+ metadata?: { // Optional (when includeMetadata = true)
1106
+ dbName: string;
1107
+ version: number;
1108
+ exportedAt: string; // ISO 8601 timestamp
1109
+ tables: string[];
1110
+ };
1111
+ data: {
1112
+ [tableName: string]: Array<Record<string, any>>;
1113
+ };
1114
+ }
1115
+ ```
1116
+
1117
+ > **Note:**
1118
+ >
1119
+ > - Automatically triggers a file download in the browser.
1120
+ > - Exported data includes all records from specified tables.
1121
+ > - Use for backup, debugging, or data migration.
1122
+ > - File download works in browser environments only.
1123
+
830
1124
  ---
831
1125
 
832
1126
  ### Schema Functions
@@ -1028,6 +1322,40 @@ const emailColumn = column.text().validate((val) => { /* ... */ });
1028
1322
  const validatorFn = emailColumn[ValidateFn]; // Function reference
1029
1323
  ```
1030
1324
 
1325
+ #### `onUpdate<T>(updater: (currentValue: T) => T): Column`
1326
+
1327
+ Sets a function to auto-update the column value during update operations.
1328
+
1329
+ ```typescript
1330
+ const schema = defineSchema({
1331
+ users: {
1332
+ id: column.int().pk().auto(),
1333
+ name: column.text(),
1334
+ updatedAt: column.timestamp().onUpdate(() => getTimestamp()),
1335
+ },
1336
+ });
1337
+ ```
1338
+
1339
+ > **Note:**
1340
+ >
1341
+ > - The updater function is called automatically during update operations.
1342
+ > - **Important**: It overrides any value provided during updates.
1343
+ > - It receives the current value of the column and should return the updated value.
1344
+ > - This is useful for fields like `"updatedAt"` timestamps that need to be refreshed on each update.
1345
+ > - If multiple updaters are chained, only the last one is used.
1346
+ > - Should not be used with auto-generated indexed columns like primary keys.
1347
+ > - The updated value is validated according to the column's type and custom validators (if any).
1348
+
1349
+ **Access the `OnUpdate` symbol (advanced):**
1350
+
1351
+ ```typescript
1352
+ import { OnUpdate } from 'locality-idb';
1353
+
1354
+ // Access updater function programmatically
1355
+ const updatedAtColumn = column.timestamp().onUpdate(() => getTimestamp());
1356
+ const updaterFn = updatedAtColumn[OnUpdate]; // Function reference
1357
+ ```
1358
+
1031
1359
  ---
1032
1360
 
1033
1361
  ### Query Methods
@@ -1329,6 +1657,86 @@ isTimestamp('invalid'); // false
1329
1657
  isTimestamp(123); // false
1330
1658
  ```
1331
1659
 
1660
+ #### `isUUID(value: unknown): value is UUID<UUIDVersion>`
1661
+
1662
+ Checks if a value is a valid UUID string (v1, v4, or v5).
1663
+
1664
+ **Parameters:**
1665
+
1666
+ - `value`: The value to check
1667
+
1668
+ **Returns:** `true` if the value is a valid UUID, otherwise `false`
1669
+
1670
+ **Example:**
1671
+
1672
+ ```typescript
1673
+ import { isUUID } from 'locality-idb';
1674
+
1675
+ // Valid UUIDs
1676
+ isUUID('d9428888-122b-11e8-b642-0ed5f89f718b'); // true (v1)
1677
+ isUUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8'); // true (v1)
1678
+ isUUID('550e8400-e29b-41d4-a716-446655440000'); // true (v4)
1679
+
1680
+ // Invalid formats
1681
+ isUUID('not-a-uuid'); // false
1682
+ isUUID('12345678-1234-1234-1234-123456789abc'); // false (invalid version)
1683
+ isUUID(123456789); // false
1684
+ ```
1685
+
1686
+ #### `isEmail(value: unknown): value is EmailString`
1687
+
1688
+ Checks if a value is a valid email string.
1689
+
1690
+ **Parameters:**
1691
+
1692
+ - `value`: The value to check
1693
+
1694
+ **Returns:** `true` if the value is a valid email, otherwise `false`
1695
+
1696
+ **Example:**
1697
+
1698
+ ```typescript
1699
+ import { isEmail } from 'locality-idb';
1700
+
1701
+ // Valid emails
1702
+ isEmail('user@example.com'); // true
1703
+ isEmail('first.last@sub.domain.co.uk'); // true
1704
+ isEmail('user+filter@example.org'); // true
1705
+
1706
+ // Invalid emails
1707
+ isEmail('plain-string'); // false
1708
+ isEmail('user@.com'); // false
1709
+ isEmail('@example.com'); // false
1710
+ isEmail('user@domain'); // false
1711
+ isEmail(12345); // false
1712
+ ```
1713
+
1714
+ #### `isURL(value: unknown): value is URLString`
1715
+
1716
+ Checks if a value is a valid URL string.
1717
+
1718
+ **Parameters:**
1719
+
1720
+ - `value`: The value to check
1721
+
1722
+ **Returns:** `true` if the value is a valid URL, otherwise `false`
1723
+
1724
+ **Example:**
1725
+
1726
+ ```typescript
1727
+ import { isURL } from 'locality-idb';
1728
+
1729
+ // Valid URLs
1730
+ isURL('https://example.com'); // true
1731
+ isURL('ftp://files.test/path?q=1'); // true
1732
+
1733
+ // Invalid URLs
1734
+ isURL('example.com'); // false (missing protocol)
1735
+ isURL('http://'); // false (empty domain)
1736
+ isURL('//cdn.domain/image.png'); // false (`URL` constructor cannot parse it)
1737
+ isURL(123456); // false
1738
+ ```
1739
+
1332
1740
  #### `openDBWithStores(name: string, stores: StoreConfig[], version?: number): Promise<IDBDatabase>`
1333
1741
 
1334
1742
  Opens an IndexedDB database with specified stores (low-level API).
@@ -1530,6 +1938,48 @@ type Selected = SelectFields<User, { name: true; email: true }>;
1530
1938
  // { name: string; email: string }
1531
1939
  ```
1532
1940
 
1941
+ ### Transaction & Export Types
1942
+
1943
+ ```typescript
1944
+ import type {
1945
+ TransactionContext,
1946
+ TransactionCallback,
1947
+ ExportOptions,
1948
+ ExportData,
1949
+ } from 'locality-idb';
1950
+
1951
+ // TransactionContext: Context object provided to transaction callback
1952
+ type TxContext = TransactionContext<Schema, TableName, ['users', 'posts']>;
1953
+
1954
+ // TransactionCallback: Function signature for transaction operations
1955
+ type TxCallback = TransactionCallback<Schema, TableName, ['users']>;
1956
+
1957
+ // ExportOptions: Configuration options for database export
1958
+ type ExportOpts = ExportOptions<'users' | 'posts'>;
1959
+ /*
1960
+ {
1961
+ tables?: ('users' | 'posts')[];
1962
+ filename?: string;
1963
+ pretty?: boolean;
1964
+ includeMetadata?: boolean;
1965
+ }
1966
+ */
1967
+
1968
+ // ExportData: Structure of exported database data
1969
+ type Exported = ExportData;
1970
+ /*
1971
+ {
1972
+ metadata?: {
1973
+ dbName: string;
1974
+ version: number;
1975
+ exportedAt: Timestamp;
1976
+ tables: string[];
1977
+ };
1978
+ data: Record<string, GenericObject[]>;
1979
+ }
1980
+ */
1981
+ ```
1982
+
1533
1983
  ---
1534
1984
 
1535
1985
  ## 📄 License