schema-idb 0.0.4 → 0.0.6

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
@@ -2,10 +2,12 @@
2
2
 
3
3
  A type-safe IndexedDB layer that brings structure to client-side storage.
4
4
 
5
+ [Live Example](https://stackblitz.com/edit/schema-idb)
6
+
5
7
  ```ts
6
8
  const db = openDB({
7
9
  name: "MyApp",
8
- version: 1,
10
+ versionStrategy: "auto",
9
11
  stores: [usersStore] as const,
10
12
  });
11
13
 
@@ -13,8 +15,6 @@ await db.users.put({ id: "u1", name: "Kim", email: "kim@example.com" });
13
15
  const user = await db.users.get("u1");
14
16
  ```
15
17
 
16
- [Live Example](https://stackblitz.com/edit/schema-idb)
17
-
18
18
  ---
19
19
 
20
20
  ## What schema-idb provides
@@ -60,7 +60,7 @@ import { openDB } from "schema-idb";
60
60
 
61
61
  const db = openDB({
62
62
  name: "MyApp",
63
- version: 1,
63
+ versionStrategy: "auto",
64
64
  stores: [usersStore] as const,
65
65
  });
66
66
  ```
@@ -223,6 +223,10 @@ const user = await db.users
223
223
  schema-idb exposes transactions as synchronous write batches across multiple stores.
224
224
 
225
225
  ```ts
226
+ // Single store
227
+ const tx = db.startTransaction("accounts");
228
+
229
+ // Multiple stores
226
230
  const tx = db.startTransaction(["accounts", "logs"]);
227
231
 
228
232
  // Queue operations (no await between them)
@@ -235,9 +239,12 @@ await tx.commit();
235
239
 
236
240
  // Or abort
237
241
  tx.abort();
242
+
243
+ // Access underlying IDBTransaction if needed
244
+ tx.raw;
238
245
  ```
239
246
 
240
- Read operations are not available inside transactions. IndexedDB transactions auto-commit after any `await`, so reads must happen outside the transaction.
247
+ Read operations are not available inside transactions. IndexedDB transactions auto-commit after any `await`, so schema-idb only supports synchronous write batching.
241
248
 
242
249
  ---
243
250
 
@@ -312,11 +319,55 @@ const db = openDB({
312
319
  - Index modifications
313
320
  - Index deletions
314
321
 
315
- ### Requires manual migration (throws error)
322
+ ### Requires manual migration (throws error by default)
316
323
 
317
324
  - Store deletions (data loss)
318
325
  - keyPath changes (requires store recreation)
319
326
 
327
+ ### Handling removed stores
328
+
329
+ When a store is removed from the schema, you can choose how to handle it:
330
+
331
+ ```ts
332
+ const db = openDB({
333
+ name: "MyApp",
334
+ versionStrategy: "auto",
335
+ // 'error' (default): Throws an error when stores are removed
336
+ // 'preserve': Renames removed stores to __storeName_deleted_v{version}__ as backup.
337
+ // Preserved stores are isolated from the typed API to avoid future name collisions.
338
+ removedStoreStrategy: "preserve",
339
+ stores: [usersStore] as const,
340
+ });
341
+ ```
342
+
343
+ #### Behavior with explicit versioning
344
+
345
+ When `versionStrategy` is `"explicit"`:
346
+
347
+ - Schema changes are **detected** but **NOT applied** automatically
348
+ - `removedStoreStrategy` is evaluated for preview purposes only
349
+ - A warning is logged if schema changes are detected but version is not bumped
350
+
351
+ ```
352
+ [schema-idb] Schema changes detected but version not bumped:
353
+ - Rename store "oldStore" to "__oldStore_deleted_v2__"
354
+ Current DB version: 1, Provided version: 1
355
+ Bump the version to apply these changes.
356
+ ```
357
+
358
+ **Important:** `removedStoreStrategy` does not perform migrations in explicit mode. It only describes what _would_ happen after a version bump. To apply the changes, increment the `version` number.
359
+
360
+ To explicitly delete a store (including backups), use a migration:
361
+
362
+ ```ts
363
+ const usersStore = defineStore("users", {
364
+ // ...
365
+ }).addMigration("003-delete-old-store", (db) => {
366
+ db.deleteObjectStore("oldStore");
367
+ db.deleteObjectStore("__oldStore_deleted_v2__"); // Remove backup too
368
+ });
369
+ ```
370
+
320
371
  ---
321
372
 
322
373
  ## Type Inference
@@ -333,39 +384,689 @@ type User = InferStore<typeof usersStore>;
333
384
 
334
385
  ## API Reference
335
386
 
336
- ### Database
387
+ > This section is intended as a complete, authoritative reference.
388
+ > Most users will not need to read it top-to-bottom.
389
+ > For a guided introduction and examples, see the sections above.
390
+
391
+ ### openDB
392
+
393
+ Opens a database connection with the given configuration.
394
+
395
+ ```ts
396
+ function openDB<T extends readonly SchemaStoreDefinition[]>(options: {
397
+ name: string;
398
+ stores: T;
399
+ versionStrategy?: "auto" | "explicit";
400
+ version?: number;
401
+ removedStoreStrategy?: "error" | "preserve";
402
+ }): SchemaDatabase<T>;
403
+ ```
404
+
405
+ | Option | Type | Description |
406
+ | ------ | ---- | ----------- |
407
+ | `name` | `string` | Database name |
408
+ | `stores` | `readonly SchemaStoreDefinition[]` | Store definitions created with `defineStore` |
409
+ | `versionStrategy` | `"auto" \| "explicit"` | `"auto"` detects schema changes automatically. Default: `"explicit"` (recommended for production control) |
410
+ | `version` | `number` | Required when `versionStrategy` is `"explicit"` |
411
+ | `removedStoreStrategy` | `"error" \| "preserve"` | How to handle removed stores. Default: `"error"` |
412
+
413
+ ### SchemaDatabase
414
+
415
+ The database object returned by `openDB`.
416
+
417
+ | Property | Type | Description |
418
+ | -------- | ---- | ----------- |
419
+ | `name` | `string` | Database name |
420
+ | `version` | `number` | Current schema version |
421
+ | `ready` | `boolean` | Whether the database is ready |
422
+ | `raw` | `IDBDatabase` | Underlying IndexedDB instance |
423
+ | `[storeName]` | `StoreAccessor` | Direct access to stores (e.g., `db.users`) |
424
+
425
+ | Method | Signature | Description |
426
+ | ------ | --------- | ----------- |
427
+ | `waitForReady` | `() => Promise<void>` | Wait for database initialization |
428
+ | `close` | `() => void` | Close the database connection |
429
+ | `startTransaction` | `(stores, options?) => Transaction` | Start a multi-store transaction |
430
+
431
+ ### Store Accessor
432
+
433
+ Each store is accessible as a property on the database object (e.g., `db.users`).
434
+
435
+ #### get
436
+
437
+ ```ts
438
+ get(key: K): Promise<T | undefined>
439
+ ```
440
+
441
+ | Param | Type | Description |
442
+ | ----- | ---- | ----------- |
443
+ | `key` | `K` | Primary key value |
444
+
445
+ Returns the record matching the key, or `undefined` if not found.
446
+
447
+ #### getAll
448
+
449
+ ```ts
450
+ getAll(): Promise<T[]>
451
+ ```
452
+
453
+ Returns all records in the store.
454
+
455
+ #### getBy
456
+
457
+ ```ts
458
+ getBy<I extends IndexedFields>(indexName: I, query: V | IDBKeyRange): Promise<T | undefined>
459
+ ```
460
+
461
+ | Param | Type | Description |
462
+ | ----- | ---- | ----------- |
463
+ | `indexName` | `I` | Name of the index to query |
464
+ | `query` | `V \| IDBKeyRange` | Value to match or key range |
465
+
466
+ Returns the first record matching the index value.
467
+
468
+ #### getAllBy
469
+
470
+ ```ts
471
+ getAllBy<I extends IndexedFields>(indexName: I, query?: V | IDBKeyRange): Promise<T[]>
472
+ ```
473
+
474
+ | Param | Type | Description |
475
+ | ----- | ---- | ----------- |
476
+ | `indexName` | `I` | Name of the index to query |
477
+ | `query` | `V \| IDBKeyRange` | Value to match or key range (optional) |
478
+
479
+ Returns all records matching the index value. If `query` is omitted, returns all records ordered by the index.
480
+
481
+ #### put
482
+
483
+ ```ts
484
+ put(value: T, key?: K): Promise<K>
485
+ ```
486
+
487
+ | Param | Type | Description |
488
+ | ----- | ---- | ----------- |
489
+ | `value` | `T` | Record to insert or update |
490
+ | `key` | `K` | Optional key (only needed if store has no keyPath) |
491
+
492
+ Inserts a new record or updates an existing one. Returns the primary key.
493
+
494
+ #### add
495
+
496
+ ```ts
497
+ add(value: T, key?: K): Promise<K>
498
+ ```
499
+
500
+ | Param | Type | Description |
501
+ | ----- | ---- | ----------- |
502
+ | `value` | `T` | Record to insert |
503
+ | `key` | `K` | Optional key (only needed if store has no keyPath) |
504
+
505
+ Inserts a new record. Throws an error if the key already exists.
506
+
507
+ #### delete
508
+
509
+ ```ts
510
+ delete(key: K | IDBKeyRange): Promise<void>
511
+ ```
512
+
513
+ | Param | Type | Description |
514
+ | ----- | ---- | ----------- |
515
+ | `key` | `K \| IDBKeyRange` | Primary key or key range to delete |
516
+
517
+ Deletes record(s) matching the key or range.
518
+
519
+ #### clear
520
+
521
+ ```ts
522
+ clear(): Promise<void>
523
+ ```
524
+
525
+ Deletes all records in the store.
526
+
527
+ #### count
528
+
529
+ ```ts
530
+ count(query?: IDBKeyRange | IDBValidKey): Promise<number>
531
+ ```
532
+
533
+ | Param | Type | Description |
534
+ | ----- | ---- | ----------- |
535
+ | `query` | `IDBKeyRange \| IDBValidKey` | Optional key or range to count |
536
+
537
+ Returns the number of records. If `query` is provided, counts only matching records.
538
+
539
+ #### query
540
+
541
+ ```ts
542
+ query(options: QueryOptions): Promise<T[]>
543
+ query(): QueryBuilder
544
+ ```
545
+
546
+ | Param | Type | Description |
547
+ | ----- | ---- | ----------- |
548
+ | `options` | `QueryOptions` | Query configuration (optional) |
549
+
550
+ When called with options, executes the query and returns results. When called without arguments, returns a `QueryBuilder` for chaining.
551
+
552
+ ### Query Options
553
+
554
+ Used with `db.store.query(options)`.
555
+
556
+ ```ts
557
+ interface QueryOptions {
558
+ index: string;
559
+ where?: WhereCondition;
560
+ orderBy?: "asc" | "desc";
561
+ limit?: number;
562
+ offset?: number;
563
+ }
564
+ ```
565
+
566
+ | Option | Type | Description |
567
+ | ------ | ---- | ----------- |
568
+ | `index` | `string` | Index name to query on |
569
+ | `where` | `WhereCondition` | Filter conditions (optional) |
570
+ | `orderBy` | `"asc" \| "desc"` | Sort order. Default: `"asc"` |
571
+ | `limit` | `number` | Maximum number of results (optional) |
572
+ | `offset` | `number` | Number of results to skip. Default: `0` |
573
+
574
+ #### WhereCondition
575
+
576
+ ```ts
577
+ interface WhereCondition {
578
+ eq?: T;
579
+ gt?: T;
580
+ gte?: T;
581
+ lt?: T;
582
+ lte?: T;
583
+ between?: [T, T];
584
+ startsWith?: string;
585
+ }
586
+ ```
587
+
588
+ | Option | Type | Description |
589
+ | ------ | ---- | ----------- |
590
+ | `eq` | `T` | Exact match |
591
+ | `gt` | `T` | Greater than |
592
+ | `gte` | `T` | Greater than or equal |
593
+ | `lt` | `T` | Less than |
594
+ | `lte` | `T` | Less than or equal |
595
+ | `between` | `[T, T]` | Inclusive range `[lower, upper]` |
596
+ | `startsWith` | `string` | Prefix match (string indexes only) |
597
+
598
+ ### Query Builder
599
+
600
+ Returned when calling `db.store.query()` without arguments.
601
+
602
+ #### index
603
+
604
+ ```ts
605
+ index<I extends IndexedFields>(name: I): IndexQueryBuilder
606
+ ```
607
+
608
+ | Param | Type | Description |
609
+ | ----- | ---- | ----------- |
610
+ | `name` | `I` | Index name to query on |
611
+
612
+ Returns an `IndexQueryBuilder` for the specified index.
613
+
614
+ #### key
615
+
616
+ ```ts
617
+ key(): IndexQueryBuilder
618
+ ```
619
+
620
+ Returns an `IndexQueryBuilder` that queries by primary key.
621
+
622
+ #### findAll
623
+
624
+ ```ts
625
+ findAll(): Promise<T[]>
626
+ ```
627
+
628
+ Executes the query and returns all matching records.
629
+
630
+ ### IndexQueryBuilder
631
+
632
+ Provides condition methods for filtering. All methods return a `FinalQueryBuilder`.
633
+
634
+ #### equals
635
+
636
+ ```ts
637
+ equals(value: V): FinalQueryBuilder
638
+ ```
639
+
640
+ | Param | Type | Description |
641
+ | ----- | ---- | ----------- |
642
+ | `value` | `V` | Value to match exactly |
643
+
644
+ #### gt / gte / lt / lte
645
+
646
+ ```ts
647
+ gt(value: V): FinalQueryBuilder // Greater than
648
+ gte(value: V): FinalQueryBuilder // Greater than or equal
649
+ lt(value: V): FinalQueryBuilder // Less than
650
+ lte(value: V): FinalQueryBuilder // Less than or equal
651
+ ```
652
+
653
+ | Param | Type | Description |
654
+ | ----- | ---- | ----------- |
655
+ | `value` | `V` | Boundary value for comparison |
656
+
657
+ #### between
658
+
659
+ ```ts
660
+ between(lower: V, upper: V): FinalQueryBuilder
661
+ ```
662
+
663
+ | Param | Type | Description |
664
+ | ----- | ---- | ----------- |
665
+ | `lower` | `V` | Lower bound (inclusive) |
666
+ | `upper` | `V` | Upper bound (inclusive) |
667
+
668
+ #### startsWith
669
+
670
+ ```ts
671
+ startsWith(prefix: string): FinalQueryBuilder
672
+ ```
673
+
674
+ | Param | Type | Description |
675
+ | ----- | ---- | ----------- |
676
+ | `prefix` | `string` | Prefix to match |
677
+
678
+ Only available for string indexes.
679
+
680
+ ### FinalQueryBuilder
681
+
682
+ Provides result modifiers and execution methods.
683
+
684
+ #### orderBy
685
+
686
+ ```ts
687
+ orderBy(order: "asc" | "desc"): FinalQueryBuilder
688
+ ```
689
+
690
+ | Param | Type | Description |
691
+ | ----- | ---- | ----------- |
692
+ | `order` | `"asc" \| "desc"` | Sort direction |
693
+
694
+ #### limit
695
+
696
+ ```ts
697
+ limit(count: number): FinalQueryBuilder
698
+ ```
699
+
700
+ | Param | Type | Description |
701
+ | ----- | ---- | ----------- |
702
+ | `count` | `number` | Maximum number of results |
703
+
704
+ #### offset
705
+
706
+ ```ts
707
+ offset(count: number): FinalQueryBuilder
708
+ ```
709
+
710
+ | Param | Type | Description |
711
+ | ----- | ---- | ----------- |
712
+ | `count` | `number` | Number of results to skip |
713
+
714
+ #### findAll
715
+
716
+ ```ts
717
+ findAll(): Promise<T[]>
718
+ ```
719
+
720
+ Executes the query and returns all matching records.
721
+
722
+ #### find
723
+
724
+ ```ts
725
+ find(): Promise<T | undefined>
726
+ ```
727
+
728
+ Executes the query and returns the first matching record, or `undefined` if none found.
729
+
730
+ #### count
731
+
732
+ ```ts
733
+ count(): Promise<number>
734
+ ```
735
+
736
+ Returns the number of matching records without fetching them.
737
+
738
+ ### Transaction
739
+
740
+ #### startTransaction
741
+
742
+ ```ts
743
+ startTransaction(
744
+ stores: string | string[],
745
+ options?: TransactionOptions
746
+ ): Transaction
747
+ ```
748
+
749
+ | Param | Type | Description |
750
+ | ----- | ---- | ----------- |
751
+ | `stores` | `string \| string[]` | Store name(s) to include in the transaction |
752
+ | `options` | `TransactionOptions` | Transaction configuration (optional) |
753
+
754
+ ##### TransactionOptions
755
+
756
+ | Option | Type | Description |
757
+ | ------ | ---- | ----------- |
758
+ | `mode` | `"write"` | Transaction mode. Currently only `"write"` is supported |
759
+ | `durability` | `"default" \| "strict" \| "relaxed"` | Durability hint for the transaction. Default: `"default"` |
760
+
761
+ #### Transaction Object
762
+
763
+ | Property | Type | Description |
764
+ | -------- | ---- | ----------- |
765
+ | `raw` | `IDBTransaction` | Underlying IndexedDB transaction |
766
+ | `[storeName]` | `TransactionStoreAccessor` | Synchronous store accessor for each included store |
767
+
768
+ ##### commit
769
+
770
+ ```ts
771
+ commit(): Promise<void>
772
+ ```
773
+
774
+ Commits all queued operations and waits for completion.
775
+
776
+ ##### abort
777
+
778
+ ```ts
779
+ abort(): void
780
+ ```
781
+
782
+ Aborts the transaction, discarding all queued operations.
783
+
784
+ #### TransactionStoreAccessor
785
+
786
+ Synchronous operations for use within transactions. Operations are queued and executed when `commit()` is called.
787
+
788
+ ##### put
789
+
790
+ ```ts
791
+ put(value: T, key?: K): void
792
+ ```
793
+
794
+ | Param | Type | Description |
795
+ | ----- | ---- | ----------- |
796
+ | `value` | `T` | Record to insert or update |
797
+ | `key` | `K` | Optional key (only needed if store has no keyPath) |
798
+
799
+ ##### add
800
+
801
+ ```ts
802
+ add(value: T, key?: K): void
803
+ ```
804
+
805
+ | Param | Type | Description |
806
+ | ----- | ---- | ----------- |
807
+ | `value` | `T` | Record to insert |
808
+ | `key` | `K` | Optional key (only needed if store has no keyPath) |
809
+
810
+ ##### delete
811
+
812
+ ```ts
813
+ delete(key: K | IDBKeyRange): void
814
+ ```
815
+
816
+ | Param | Type | Description |
817
+ | ----- | ---- | ----------- |
818
+ | `key` | `K \| IDBKeyRange` | Primary key or key range to delete |
819
+
820
+ ##### clear
821
+
822
+ ```ts
823
+ clear(): void
824
+ ```
825
+
826
+ Queues deletion of all records in the store.
827
+
828
+ ### defineStore
829
+
830
+ Creates a store definition with schema.
831
+
832
+ ```ts
833
+ defineStore<N extends string, S extends StoreSchema>(
834
+ name: N,
835
+ schema: S
836
+ ): SchemaStoreDefinition<N, S>
837
+ ```
838
+
839
+ | Param | Type | Description |
840
+ | ----- | ---- | ----------- |
841
+ | `name` | `N` | Store name (used as `db.name` accessor) |
842
+ | `schema` | `S` | Object defining fields using `field` builders |
843
+
844
+ Returns a `SchemaStoreDefinition` with the following method:
845
+
846
+ #### addMigration
847
+
848
+ ```ts
849
+ addMigration(
850
+ name: string,
851
+ fn: (db: IDBDatabase, tx: IDBTransaction) => void
852
+ ): this
853
+ ```
854
+
855
+ | Param | Type | Description |
856
+ | ----- | ---- | ----------- |
857
+ | `name` | `string` | Unique migration identifier (sorted alphabetically) |
858
+ | `fn` | `MigrationFn` | Migration function with access to database and transaction |
859
+
860
+ Migrations run during version upgrades in alphabetical order by name.
861
+
862
+ ### field
863
+
864
+ Field type builders for schema definition.
865
+
866
+ #### field.string
867
+
868
+ ```ts
869
+ field.string(): FieldBuilder<string>
870
+ ```
871
+
872
+ Creates a string field.
873
+
874
+ #### field.number
875
+
876
+ ```ts
877
+ field.number(): FieldBuilder<number>
878
+ ```
879
+
880
+ Creates a number field.
881
+
882
+ #### field.boolean
337
883
 
338
884
  ```ts
339
- const db = openDB({ name, version, stores });
885
+ field.boolean(): FieldBuilder<boolean>
886
+ ```
887
+
888
+ Creates a boolean field.
340
889
 
341
- db.waitForReady(); // Promise<void>
342
- db.ready; // boolean
343
- db.close(); // Close connection
344
- db.version; // Current version number
345
- db.raw; // Underlying IDBDatabase
890
+ #### field.date
891
+
892
+ ```ts
893
+ field.date(): FieldBuilder<Date>
346
894
  ```
347
895
 
348
- ### Store operations
896
+ Creates a Date field.
897
+
898
+ #### field.object
349
899
 
350
900
  ```ts
351
- db.store.get(key); // Get by primary key
352
- db.store.getAll(); // Get all records
353
- db.store.getAllByIndex(index, value); // Get by index value
354
- db.store.put(value); // Insert or update
355
- db.store.add(value); // Insert (fails if exists)
356
- db.store.delete(key); // Delete by key
357
- db.store.clear(); // Delete all
358
- db.store.count(); // Count records
359
- db.store.query(options); // Query with conditions
901
+ field.object<S>(schema: (t: TypeFactory) => S): FieldBuilder<InferObjectType<S>>
360
902
  ```
361
903
 
362
- ### Utilities
904
+ | Param | Type | Description |
905
+ | ----- | ---- | ----------- |
906
+ | `schema` | `(t: TypeFactory) => S` | Function returning an object schema using type builders |
907
+
908
+ Creates a nested object field.
363
909
 
364
910
  ```ts
365
- import { deleteDB, isIndexedDBAvailable } from "schema-idb";
911
+ field.object(t => ({
912
+ street: t.string(),
913
+ city: t.string(),
914
+ zipCode: t.number().optional(),
915
+ }))
916
+ ```
917
+
918
+ #### field.tuple
366
919
 
367
- await deleteDB("MyApp");
368
- isIndexedDBAvailable();
920
+ ```ts
921
+ field.tuple<T>(schema: (t: TypeFactory) => T): FieldBuilder<InferTupleType<T>>
922
+ ```
923
+
924
+ | Param | Type | Description |
925
+ | ----- | ---- | ----------- |
926
+ | `schema` | `(t: TypeFactory) => T` | Function returning a tuple schema as array |
927
+
928
+ Creates a fixed-length tuple field.
929
+
930
+ ```ts
931
+ field.tuple(t => [t.number(), t.number()]) // [number, number]
932
+ ```
933
+
934
+ #### field.enum
935
+
936
+ ```ts
937
+ field.enum<T extends readonly string[]>(values: T): FieldBuilder<T[number]>
938
+ ```
939
+
940
+ | Param | Type | Description |
941
+ | ----- | ---- | ----------- |
942
+ | `values` | `readonly string[]` | Array of allowed string values |
943
+
944
+ Creates a string union type field.
945
+
946
+ ```ts
947
+ field.enum(['active', 'inactive', 'pending'] as const)
948
+ ```
949
+
950
+ #### field.nativeEnum
951
+
952
+ ```ts
953
+ field.nativeEnum<T extends Record<string, string | number>>(enumObj: T): FieldBuilder<T[keyof T]>
954
+ ```
955
+
956
+ | Param | Type | Description |
957
+ | ----- | ---- | ----------- |
958
+ | `enumObj` | `T` | TypeScript enum object |
959
+
960
+ Creates a field from a TypeScript enum.
961
+
962
+ ```ts
963
+ enum Status { Active = 'active', Inactive = 'inactive' }
964
+ field.nativeEnum(Status)
965
+ ```
966
+
967
+ ### FieldBuilder
968
+
969
+ Methods available on all field builders. All methods return `this` for chaining.
970
+
971
+ #### primaryKey
972
+
973
+ ```ts
974
+ primaryKey(): FieldBuilder
975
+ ```
976
+
977
+ Marks the field as the store's primary key. Exactly one field per store must be marked as primary key.
978
+
979
+ #### index
980
+
981
+ ```ts
982
+ index(options?: IndexOptions): FieldBuilder
983
+ ```
984
+
985
+ | Param | Type | Description |
986
+ | ----- | ---- | ----------- |
987
+ | `options.unique` | `boolean` | If `true`, enforces unique values. Default: `false` |
988
+ | `options.multiEntry` | `boolean` | If `true`, indexes each array element separately. Default: `false` |
989
+
990
+ Creates an index on this field, enabling queries via `query()`, `getBy()`, and `getAllBy()`.
991
+
992
+ #### optional
993
+
994
+ ```ts
995
+ optional(): FieldBuilder
996
+ ```
997
+
998
+ Marks the field as optional, allowing `undefined` values.
999
+
1000
+ #### default
1001
+
1002
+ ```ts
1003
+ default(value: T | (() => T)): FieldBuilder
1004
+ ```
1005
+
1006
+ | Param | Type | Description |
1007
+ | ----- | ---- | ----------- |
1008
+ | `value` | `T \| (() => T)` | Default value or factory function |
1009
+
1010
+ Sets a default value applied on read when the field is missing. Factory functions are called for each read.
1011
+
1012
+ ```ts
1013
+ field.number().default(0)
1014
+ field.date().default(() => new Date())
1015
+ ```
1016
+
1017
+ #### array
1018
+
1019
+ ```ts
1020
+ array(): FieldBuilder<T[]>
1021
+ ```
1022
+
1023
+ Converts the field type to an array.
1024
+
1025
+ ```ts
1026
+ field.string().array() // string[]
1027
+ ```
1028
+
1029
+ ### Utility Functions
1030
+
1031
+ #### deleteDB
1032
+
1033
+ ```ts
1034
+ deleteDB(name: string): Promise<void>
1035
+ ```
1036
+
1037
+ | Param | Type | Description |
1038
+ | ----- | ---- | ----------- |
1039
+ | `name` | `string` | Database name to delete |
1040
+
1041
+ Deletes the database and all its data.
1042
+
1043
+ #### isIndexedDBAvailable
1044
+
1045
+ ```ts
1046
+ isIndexedDBAvailable(): boolean
1047
+ ```
1048
+
1049
+ Returns `true` if IndexedDB is available in the current environment.
1050
+
1051
+ ### Type Utilities
1052
+
1053
+ #### InferStore
1054
+
1055
+ ```ts
1056
+ type InferStore<T> = /* inferred output type from store definition */
1057
+ ```
1058
+
1059
+ Extracts the TypeScript type from a store definition:
1060
+
1061
+ ```ts
1062
+ const usersStore = defineStore("users", {
1063
+ id: field.string().primaryKey(),
1064
+ name: field.string(),
1065
+ age: field.number().optional().default(0),
1066
+ });
1067
+
1068
+ type User = InferStore<typeof usersStore>;
1069
+ // { id: string; name: string; age: number }
369
1070
  ```
370
1071
 
371
1072
  ---
@@ -1,4 +1,4 @@
1
- import type { StoreSchema, InferInput, InferOutput, PrimaryKeyType, IndexedFields } from './field.js';
1
+ import type { StoreSchema, InferInput, InferOutput, PrimaryKeyType, IndexedFields, IndexFieldTypes } from './field.js';
2
2
  import type { SchemaStoreDefinition } from './schema.js';
3
3
  import type { TypedQueryOptions, TypedQueryBuilder } from './query.js';
4
4
  import type { Transaction, TransactionOptions } from './transaction.js';
@@ -7,7 +7,8 @@ type StoreNames<TStores extends readonly AnySchemaStore[]> = TStores[number]['na
7
7
  export interface SchemaStoreAccessor<S extends StoreSchema> {
8
8
  get(key: PrimaryKeyType<S>): Promise<InferOutput<S> | undefined>;
9
9
  getAll(): Promise<InferOutput<S>[]>;
10
- getAllByIndex<I extends IndexedFields<S> & string>(indexName: I, query?: IDBKeyRange | IDBValidKey): Promise<InferOutput<S>[]>;
10
+ getBy<I extends IndexedFields<S> & string>(indexName: I, query: IDBKeyRange | IndexFieldTypes<S>[I]): Promise<InferOutput<S> | undefined>;
11
+ getAllBy<I extends IndexedFields<S> & string>(indexName: I, query?: IDBKeyRange | IndexFieldTypes<S>[I]): Promise<InferOutput<S>[]>;
11
12
  put(value: InferInput<S>, key?: PrimaryKeyType<S>): Promise<PrimaryKeyType<S>>;
12
13
  add(value: InferInput<S>, key?: PrimaryKeyType<S>): Promise<PrimaryKeyType<S>>;
13
14
  delete(key: PrimaryKeyType<S> | IDBKeyRange): Promise<void>;
@@ -20,6 +21,7 @@ export interface SchemaDBConfig<TStores extends readonly AnySchemaStore[]> {
20
21
  name: string;
21
22
  version?: number;
22
23
  versionStrategy?: 'explicit' | 'auto';
24
+ removedStoreStrategy?: 'error' | 'preserve';
23
25
  stores: TStores;
24
26
  onBlocked?: () => void;
25
27
  onVersionChange?: () => void;
@@ -1 +1,9 @@
1
- "use strict";var P=Object.defineProperty;var c=(e,r)=>P(e,"name",{value:r,configurable:!0});import{openDatabase as T}from"./utils.js";import{createStoreAccessor as b}from"./storeAccessor.js";import{createStartTransaction as v}from"./transaction.js";import{determineAutoVersion as D,applySafeChanges as E,openDatabaseForSchemaRead as h}from"./schemaDetection.js";import{ensureSchemaHistoryStore as A,getAppliedMigrations as p,recordMigrationApplied as R,initializeSchemaHistory as x}from"./migrationHistory.js";function G(e){return e}c(G,"getKeyPathString");function z(e,r,o){const t=c(async()=>{if(await e.readyPromise,!e.idb)throw new Error("Database initialization failed");return b(e.idb,r,o)},"getAccessor");return new Proxy({},{get(n,d){return d==="query"?s=>s?t().then(i=>i.query(s)):M(e,r,o):async(...s)=>(await t())[d](...s)}})}c(z,"createLazyStoreAccessor");function M(e,r,o){const t=c(async()=>{if(await e.readyPromise,!e.idb)throw new Error("Database initialization failed");return b(e.idb,r,o).query()},"getQueryBuilder"),n=c(d=>new Proxy({},{get(s,i){return i==="findAll"||i==="find"||i==="count"?()=>d().then(a=>a[i]()):(...a)=>n(c(()=>d().then(l=>l[i](...a)),"newGetBuilder"))}}),"createBuilderProxy");return n(t)}c(M,"createLazyQueryBuilder");function k(e,r){const o={get name(){return e.idb?.name??r},get version(){return e.idb?.version??0},get raw(){if(!e.idb)throw new Error("Database not ready. Call waitForReady() first or check ready property.");return e.idb},get ready(){return e.ready},waitForReady(){return e.readyPromise},close(){e.idb?.close()},startTransaction(...t){if(!e.startTransaction){const[n,d]=t,s=Array.isArray(n)?n:[n];return j(e,s,d)}return e.startTransaction(...t)}};for(const t of e.stores)Object.defineProperty(o,t.name,{get(){return z(e,t.name,t.defaults)},enumerable:!0});return o}c(k,"buildSchemaDatabase");function j(e,r,o){const t={get raw(){throw new Error("Transaction raw is not available before ready state. Use await db.waitForReady() before starting transactions.")},async commit(){if(await e.readyPromise,!e.startTransaction)throw new Error("Database initialization failed");return e.startTransaction(r,o).commit()},abort(){}};for(const n of r)e.stores.find(s=>s.name===n)&&Object.defineProperty(t,n,{get(){throw new Error("Transaction operations before ready state are not yet supported. Use await db.waitForReady() before starting transactions.")},enumerable:!0});return t}c(j,"createLazyTransaction");function q(e){const r=[],o=new Set;for(const t of e)for(const n of t.migrations){if(o.has(n.name))throw new Error(`Duplicate migration name "${n.name}" found across stores`);o.add(n.name),r.push(n)}return r.sort((t,n)=>t.name.localeCompare(n.name))}c(q,"collectMigrations");function S(e,r){const o=new Set(r);return e.filter(t=>!o.has(t.name)).sort((t,n)=>t.name.localeCompare(n.name))}c(S,"filterPendingMigrations");function B(e,r,o,t,n,d,s){if(A(e),o===0){for(const a of t){const f=e.createObjectStore(a.name,{keyPath:a.keyPath});for(const l of a.indexes)f.createIndex(l.name,l.keyPath,{unique:l.unique??!1,multiEntry:l.multiEntry??!1})}x(r)}else s&&s.safe.length>0&&E(e,r,s.safe,t);let i=[...d];for(const a of n)try{const f=a.up(e,r);f instanceof Promise&&f.catch(l=>{console.error(`Migration "${a.name}" failed:`,l),r.abort()}),R(r,a.name,i),i=[...i,a.name].sort()}catch(f){throw console.error(`Migration "${a.name}" failed:`,f),r.abort(),f}}c(B,"handleUpgrade");export function openDB(e){const{name:r,version:o,versionStrategy:t="explicit",stores:n,onBlocked:d,onVersionChange:s}=e,i=new Set;for(const m of n){if(i.has(m.name))throw new Error(`Duplicate store name: "${m.name}"`);i.add(m.name)}const a=q(n);let f=c(()=>{},"readyResolve"),l=c(()=>{},"readyReject");const w={idb:null,ready:!1,error:null,readyPromise:new Promise((m,y)=>{f=m,l=y}),readyResolve:f,readyReject:l,stores:n,startTransaction:null},u=k(w,r);return C(w,r,o,t,n,a,d,s),u}c(openDB,"openDB");async function C(e,r,o,t,n,d,s,i){try{let a=[],f=null,l;if(t==="auto"){const u=await D(r,n);if(l=u.version,f=u.changes,u.version>1){const y=await h(r);y&&(a=await p(y),y.close())}S(d,a).length>0&&!u.needsUpgrade&&(l=u.version+1)}else{if(o===void 0)throw new Error('Version is required when versionStrategy is "explicit"');l=o;const u=await h(r);u&&(a=await p(u),u.close())}const g=S(d,a),w=await T(r,l,(u,m,y)=>{B(u,m,y,n,g,a,f)},s);i&&(w.onversionchange=i),e.idb=w,e.startTransaction=v(w,n),e.ready=!0,e.readyResolve()}catch(a){e.error=a instanceof Error?a:new Error(String(a)),e.readyReject(e.error)}}c(C,"initializeDatabase");
1
+ "use strict";var E=Object.defineProperty;var d=(e,r)=>E(e,"name",{value:r,configurable:!0});import{openDatabase as T}from"./utils.js";import{createStoreAccessor as D}from"./storeAccessor.js";import{createStartTransaction as A}from"./transaction.js";import{determineAutoVersion as k,applySafeChanges as R,openDatabaseForSchemaRead as $,readExistingSchema as M,toDesiredSchema as z,detectSchemaChanges as j}from"./schemaDetection.js";import{ensureSchemaHistoryStore as B,getAppliedMigrations as x,recordMigrationApplied as C,initializeSchemaHistory as q}from"./migrationHistory.js";function Y(e){return e}d(Y,"getKeyPathString");function U(e,r,a){const n=d(async()=>{if(await e.readyPromise,!e.idb)throw new Error("Database initialization failed");return D(e.idb,r,a)},"getAccessor");return new Proxy({},{get(t,o){return o==="query"?c=>c?n().then(u=>u.query(c)):F(e,r,a):async(...c)=>(await n())[o](...c)}})}d(U,"createLazyStoreAccessor");function F(e,r,a){const n=d(async()=>{if(await e.readyPromise,!e.idb)throw new Error("Database initialization failed");return D(e.idb,r,a).query()},"getQueryBuilder"),t=d(o=>new Proxy({},{get(c,u){return u==="findAll"||u==="find"||u==="count"?()=>o().then(s=>s[u]()):(...s)=>t(d(()=>o().then(l=>l[u](...s)),"newGetBuilder"))}}),"createBuilderProxy");return t(n)}d(F,"createLazyQueryBuilder");function K(e,r){const a={get name(){return e.idb?.name??r},get version(){return e.idb?.version??0},get raw(){if(!e.idb)throw new Error("Database not ready. Call waitForReady() first or check ready property.");return e.idb},get ready(){return e.ready},waitForReady(){return e.readyPromise},close(){e.idb?.close()},startTransaction(...n){if(!e.startTransaction){const[t,o]=n,c=Array.isArray(t)?t:[t];return L(e,c,o)}return e.startTransaction(...n)}};for(const n of e.stores)Object.defineProperty(a,n.name,{get(){return U(e,n.name,n.defaults)},enumerable:!0});return a}d(K,"buildSchemaDatabase");function L(e,r,a){const n={get raw(){throw new Error("Transaction raw is not available before ready state. Use await db.waitForReady() before starting transactions.")},async commit(){if(await e.readyPromise,!e.startTransaction)throw new Error("Database initialization failed");return e.startTransaction(r,a).commit()},abort(){}};for(const t of r)e.stores.find(c=>c.name===t)&&Object.defineProperty(n,t,{get(){throw new Error("Transaction operations before ready state are not yet supported. Use await db.waitForReady() before starting transactions.")},enumerable:!0});return n}d(L,"createLazyTransaction");function O(e){const r=[],a=new Set;for(const n of e)for(const t of n.migrations){if(a.has(t.name))throw new Error(`Duplicate migration name "${t.name}" found across stores`);a.add(t.name),r.push(t)}return r.sort((n,t)=>n.name.localeCompare(t.name))}d(O,"collectMigrations");function N(e,r){const a=new Set(r);return e.filter(n=>!a.has(n.name)).sort((n,t)=>n.name.localeCompare(t.name))}d(N,"filterPendingMigrations");function V(e,r,a,n,t,o,c){if(B(e),a===0){for(const s of n){const i=e.createObjectStore(s.name,{keyPath:s.keyPath});for(const l of s.indexes)i.createIndex(l.name,l.keyPath,{unique:l.unique??!1,multiEntry:l.multiEntry??!1})}q(r)}else c&&c.safe.length>0&&R(e,r,c.safe,n);let u=[...o];for(const s of t)try{const i=s.up(e,r);i instanceof Promise&&i.catch(l=>{console.error(`Migration "${s.name}" failed:`,l),r.abort()}),C(r,s.name,u),u=[...u,s.name].sort()}catch(i){throw console.error(`Migration "${s.name}" failed:`,i),r.abort(),i}}d(V,"handleUpgrade");export function openDB(e){const{name:r,version:a,versionStrategy:n="explicit",removedStoreStrategy:t="error",stores:o,onBlocked:c,onVersionChange:u}=e,s=new Set;for(const f of o){if(s.has(f.name))throw new Error(`Duplicate store name: "${f.name}"`);s.add(f.name)}const i=O(o);let l=d(()=>{},"readyResolve"),g=d(()=>{},"readyReject");const p={idb:null,ready:!1,error:null,readyPromise:new Promise((f,y)=>{l=f,g=y}),readyResolve:l,readyReject:g,stores:o,startTransaction:null},m=K(p,r);return G(p,r,a,n,t,o,i,c,u),m}d(openDB,"openDB");async function G(e,r,a,n,t,o,c,u,s){try{let i=[],l=null,g;if(n==="auto"){const m=await k(r,o,{removedStoreStrategy:t});if(g=m.version,l=m.changes,m.version>1){const y=await $(r);y&&(i=await x(y),y.close())}N(c,i).length>0&&!m.needsUpgrade&&(g=m.version+1)}else{if(a===void 0)throw new Error('Version is required when versionStrategy is "explicit"');g=a;const m=await $(r);if(m){const f=m.version;i=await x(m);const y=M(m),_=z(o);m.close();const b=j(y,_);if(b.hasChanges){const S=[];for(const w of b.dangerous)w.type==="store_delete"&&t==="preserve"?b.safe.push({type:"store_rename",oldName:w.storeName,newName:`__${w.storeName}_deleted_v${f}__`}):S.push(w);if(S.length>0){const h=`Dangerous schema changes detected:
2
+ ${S.map(v=>{switch(v.type){case"store_delete":return`Store "${v.storeName}" would be deleted. Use removedStoreStrategy: 'preserve' to backup, or add a migration to explicitly delete it.`;case"keypath_change":return`Store "${v.storeName}" keyPath changed from "${v.oldKeyPath}" to "${v.newKeyPath}". This requires recreating the store with a manual migration.`;default:return"Unknown dangerous change"}}).join(`
3
+ `)}
4
+
5
+ Add explicit migrations to handle these changes safely.`;throw console.error("[schema-idb]",h),new Error(h)}if(b.dangerous=S,l=b,g<=f){const w=b.safe.map(h=>{switch(h.type){case"store_add":return`- Add store "${h.storeName}"`;case"store_rename":return`- Rename store "${h.oldName}" to "${h.newName}"`;case"index_add":return`- Add index "${h.indexName}" on "${h.storeName}"`;case"index_delete":return`- Delete index "${h.indexName}" from "${h.storeName}"`;default:return"- Schema change"}});console.warn(`[schema-idb] Schema changes detected but version not bumped:
6
+ ${w.join(`
7
+ `)}
8
+ Current DB version: ${f}, Provided version: ${g}
9
+ Bump the version to apply these changes.`)}}}}const P=N(c,i),p=await T(r,g,(m,f,y)=>{V(m,f,y,o,P,i,l)},u);s&&(p.onversionchange=s),e.idb=p,e.startTransaction=A(p,o),e.ready=!0,e.readyResolve()}catch(i){e.error=i instanceof Error?i:new Error(String(i)),e.readyReject(e.error)}}d(G,"initializeDatabase");
package/dist/field.d.ts CHANGED
@@ -92,6 +92,7 @@ export type IndexedFields<S extends StoreSchema> = {
92
92
  export type IndexFieldTypes<S extends StoreSchema> = {
93
93
  [K in IndexedFields<S>]: S[K] extends FieldBuilder<infer T, boolean, boolean, boolean> ? T : never;
94
94
  };
95
+ export type FieldType<S extends StoreSchema, K extends keyof S> = S[K] extends FieldBuilder<infer T, boolean, boolean, boolean> ? T : never;
95
96
  export type InferStore<TStore> = TStore extends {
96
97
  schema: infer S;
97
98
  } ? S extends StoreSchema ? InferOutput<S> : never : never;
package/dist/query.d.ts CHANGED
@@ -9,6 +9,22 @@ export interface WhereCondition<T = unknown> {
9
9
  between?: [T, T];
10
10
  startsWith?: string;
11
11
  }
12
+ export type TypedWhereCondition<T> = T extends string ? {
13
+ eq?: T;
14
+ gt?: T;
15
+ gte?: T;
16
+ lt?: T;
17
+ lte?: T;
18
+ between?: [T, T];
19
+ startsWith?: string;
20
+ } : {
21
+ eq?: T;
22
+ gt?: T;
23
+ gte?: T;
24
+ lt?: T;
25
+ lte?: T;
26
+ between?: [T, T];
27
+ };
12
28
  export interface QueryOptions {
13
29
  index?: string;
14
30
  where?: WhereCondition;
@@ -16,13 +32,21 @@ export interface QueryOptions {
16
32
  limit?: number;
17
33
  offset?: number;
18
34
  }
19
- export interface TypedQueryOptions<S extends StoreSchema> {
20
- index?: IndexedFields<S>;
35
+ export type TypedQueryOptions<S extends StoreSchema> = {
36
+ [I in IndexedFields<S>]: {
37
+ index: I;
38
+ where?: TypedWhereCondition<IndexFieldTypes<S>[I]>;
39
+ orderBy?: SortOrder;
40
+ limit?: number;
41
+ offset?: number;
42
+ };
43
+ }[IndexedFields<S>] | {
44
+ index?: undefined;
21
45
  where?: WhereCondition<unknown>;
22
46
  orderBy?: SortOrder;
23
47
  limit?: number;
24
48
  offset?: number;
25
- }
49
+ };
26
50
  interface QueryState {
27
51
  indexName?: string;
28
52
  useKey: boolean;
@@ -125,20 +149,23 @@ export interface IndexQueryBuilder<T> {
125
149
  findAll(): Promise<T[]>;
126
150
  find(): Promise<T | undefined>;
127
151
  }
128
- export interface TypedIndexQueryBuilder<T, S extends StoreSchema, I extends IndexedFields<S>> {
129
- equals(value: IndexFieldTypes<S>[I]): FinalQueryBuilder<T>;
130
- gt(value: IndexFieldTypes<S>[I]): FinalQueryBuilder<T>;
131
- gte(value: IndexFieldTypes<S>[I]): FinalQueryBuilder<T>;
132
- lt(value: IndexFieldTypes<S>[I]): FinalQueryBuilder<T>;
133
- lte(value: IndexFieldTypes<S>[I]): FinalQueryBuilder<T>;
134
- between(lower: IndexFieldTypes<S>[I], upper: IndexFieldTypes<S>[I]): FinalQueryBuilder<T>;
135
- startsWith(prefix: IndexFieldTypes<S>[I] extends string ? string : never): FinalQueryBuilder<T>;
152
+ interface BaseIndexQueryBuilder<T, V> {
153
+ equals(value: V): FinalQueryBuilder<T>;
154
+ gt(value: V): FinalQueryBuilder<T>;
155
+ gte(value: V): FinalQueryBuilder<T>;
156
+ lt(value: V): FinalQueryBuilder<T>;
157
+ lte(value: V): FinalQueryBuilder<T>;
158
+ between(lower: V, upper: V): FinalQueryBuilder<T>;
136
159
  orderBy(order: SortOrder): FinalQueryBuilder<T>;
137
160
  limit(count: number): FinalQueryBuilder<T>;
138
161
  offset(count: number): FinalQueryBuilder<T>;
139
162
  findAll(): Promise<T[]>;
140
163
  find(): Promise<T | undefined>;
141
164
  }
165
+ interface StringIndexQueryBuilder<T> {
166
+ startsWith(prefix: string): FinalQueryBuilder<T>;
167
+ }
168
+ export type TypedIndexQueryBuilder<T, S extends StoreSchema, I extends IndexedFields<S>> = BaseIndexQueryBuilder<T, IndexFieldTypes<S>[I]> & (IndexFieldTypes<S>[I] extends string ? StringIndexQueryBuilder<T> : {});
142
169
  export interface FinalQueryBuilder<T> {
143
170
  orderBy(order: SortOrder): FinalQueryBuilder<T>;
144
171
  limit(count: number): FinalQueryBuilder<T>;
@@ -19,6 +19,10 @@ export type SchemaChangeType = {
19
19
  } | {
20
20
  type: 'store_delete';
21
21
  storeName: string;
22
+ } | {
23
+ type: 'store_rename';
24
+ oldName: string;
25
+ newName: string;
22
26
  } | {
23
27
  type: 'keypath_change';
24
28
  storeName: string;
@@ -61,19 +65,16 @@ export declare function applySafeChanges(db: IDBDatabase, tx: IDBTransaction, ch
61
65
  keyPath: string | string[] | undefined;
62
66
  indexes: IndexDefinition[];
63
67
  }[]): void;
64
- export declare function generateSchemaFingerprint(stores: readonly {
65
- name: string;
66
- keyPath: string | string[] | undefined;
67
- indexes: IndexDefinition[];
68
- }[]): string;
69
- export declare function hashFingerprint(fingerprint: string): number;
70
68
  export declare function getCurrentDatabaseVersion(dbName: string): Promise<number>;
71
69
  export declare function openDatabaseForSchemaRead(dbName: string): Promise<IDBDatabase | null>;
70
+ export interface AutoVersionOptions {
71
+ removedStoreStrategy?: 'error' | 'preserve';
72
+ }
72
73
  export declare function determineAutoVersion(dbName: string, stores: readonly {
73
74
  name: string;
74
75
  keyPath: string | string[] | undefined;
75
76
  indexes: IndexDefinition[];
76
- }[]): Promise<{
77
+ }[], options?: AutoVersionOptions): Promise<{
77
78
  version: number;
78
79
  changes: SchemaChanges | null;
79
80
  needsUpgrade: boolean;
@@ -1,5 +1,5 @@
1
- "use strict";var y=Object.defineProperty;var c=(t,n)=>y(t,"name",{value:n,configurable:!0});function x(t){return t.startsWith("__")}c(x,"isInternalStore");export function readExistingSchema(t){const n=new Map,e=Array.from({length:t.objectStoreNames.length},(a,r)=>t.objectStoreNames.item(r)).filter(a=>!x(a));for(const a of e){const r=t.transaction(a,"readonly"),o=r.objectStore(a),i=new Map,u=Array.from({length:o.indexNames.length},(s,d)=>o.indexNames.item(d));for(const s of u){const d=o.index(s);i.set(s,{keyPath:d.keyPath,unique:d.unique,multiEntry:d.multiEntry})}n.set(a,{name:a,keyPath:o.keyPath,indexes:i}),r.abort()}return n}c(readExistingSchema,"readExistingSchema");export function toDesiredSchema(t){const n=new Map;for(const e of t)n.set(e.name,{name:e.name,keyPath:e.keyPath,indexes:e.indexes});return n}c(toDesiredSchema,"toDesiredSchema");function h(t,n){return t===n||t==null&&n==null?!0:t==null||n==null?!1:Array.isArray(t)&&Array.isArray(n)?t.length!==n.length?!1:t.every((e,a)=>e===n[a]):t===n}c(h,"keyPathEquals");export function detectSchemaChanges(t,n){const e=[],a=[];for(const[r,o]of n){const i=t.get(r);if(!i){e.push({type:"store_add",storeName:r});continue}if(!h(i.keyPath,o.keyPath)){a.push({type:"keypath_change",storeName:r,oldKeyPath:i.keyPath,newKeyPath:o.keyPath});continue}for(const s of o.indexes){const d=i.indexes.get(s.name);if(!d)e.push({type:"index_add",storeName:r,indexName:s.name,index:s});else{const m=!h(d.keyPath,s.keyPath),l=d.unique!==(s.unique??!1),f=d.multiEntry!==(s.multiEntry??!1);(m||l||f)&&(e.push({type:"index_delete",storeName:r,indexName:s.name}),e.push({type:"index_add",storeName:r,indexName:s.name,index:s}))}}const u=new Set(o.indexes.map(s=>s.name));for(const s of i.indexes.keys())u.has(s)||e.push({type:"index_delete",storeName:r,indexName:s})}for(const r of t.keys())n.has(r)||a.push({type:"store_delete",storeName:r});return{safe:e,dangerous:a,hasChanges:e.length>0||a.length>0}}c(detectSchemaChanges,"detectSchemaChanges");export function applySafeChanges(t,n,e,a){for(const r of e)switch(r.type){case"store_add":{const o=a.find(i=>i.name===r.storeName);if(o){const i=t.createObjectStore(o.name,{keyPath:o.keyPath});for(const u of o.indexes)i.createIndex(u.name,u.keyPath,{unique:u.unique??!1,multiEntry:u.multiEntry??!1})}break}case"index_add":{n.objectStore(r.storeName).createIndex(r.indexName,r.index.keyPath,{unique:r.index.unique??!1,multiEntry:r.index.multiEntry??!1});break}case"index_delete":{n.objectStore(r.storeName).deleteIndex(r.indexName);break}}}c(applySafeChanges,"applySafeChanges");export function generateSchemaFingerprint(t){const n=t.map(e=>({name:e.name,keyPath:e.keyPath,indexes:e.indexes.map(a=>({name:a.name,keyPath:a.keyPath,unique:a.unique??!1,multiEntry:a.multiEntry??!1})).sort((a,r)=>a.name.localeCompare(r.name))})).sort((e,a)=>e.name.localeCompare(a.name));return JSON.stringify(n)}c(generateSchemaFingerprint,"generateSchemaFingerprint");export function hashFingerprint(t){let n=0;for(let e=0;e<t.length;e++){const a=t.charCodeAt(e);n=(n<<5)-n+a,n=n&n}return Math.abs(n)}c(hashFingerprint,"hashFingerprint");export async function getCurrentDatabaseVersion(t){return new Promise(n=>{const e=indexedDB.open(t);e.onsuccess=()=>{const a=e.result,r=a.version;a.close(),n(r)},e.onerror=()=>{n(0)}})}c(getCurrentDatabaseVersion,"getCurrentDatabaseVersion");export async function openDatabaseForSchemaRead(t){return new Promise(n=>{const e=indexedDB.open(t);e.onsuccess=()=>{n(e.result)},e.onerror=()=>{n(null)},e.onupgradeneeded=()=>{e.transaction?.abort()}})}c(openDatabaseForSchemaRead,"openDatabaseForSchemaRead");export async function determineAutoVersion(t,n){const e=await openDatabaseForSchemaRead(t);if(!e)return{version:1,changes:null,needsUpgrade:!0};const a=e.version,r=readExistingSchema(e),o=toDesiredSchema(n);e.close();const i=detectSchemaChanges(r,o);if(!i.hasChanges)return{version:a,changes:null,needsUpgrade:!1};if(i.dangerous.length>0){const u=i.dangerous.map(s=>{switch(s.type){case"store_delete":return`Store "${s.storeName}" would be deleted. Use a manual migration to handle this.`;case"keypath_change":return`Store "${s.storeName}" keyPath changed from "${s.oldKeyPath}" to "${s.newKeyPath}". This requires recreating the store with a manual migration.`;default:return"Unknown dangerous change"}});throw new Error(`Dangerous schema changes detected:
2
- ${u.join(`
1
+ "use strict";var g=Object.defineProperty;var l=(r,t)=>g(r,"name",{value:t,configurable:!0});function p(r){return r.startsWith("__")}l(p,"isInternalStore");export function readExistingSchema(r){const t=new Map,n=Array.from({length:r.objectStoreNames.length},(a,o)=>r.objectStoreNames.item(o)).filter(a=>!p(a));for(const a of n){const o=r.transaction(a,"readonly"),u=o.objectStore(a),s=new Map,c=Array.from({length:u.indexNames.length},(e,i)=>u.indexNames.item(i));for(const e of c){const i=u.index(e);s.set(e,{keyPath:i.keyPath,unique:i.unique,multiEntry:i.multiEntry})}t.set(a,{name:a,keyPath:u.keyPath,indexes:s}),o.abort()}return t}l(readExistingSchema,"readExistingSchema");export function toDesiredSchema(r){const t=new Map;for(const n of r)t.set(n.name,{name:n.name,keyPath:n.keyPath,indexes:n.indexes});return t}l(toDesiredSchema,"toDesiredSchema");function x(r,t){return r===t||r==null&&t==null?!0:r==null||t==null?!1:Array.isArray(r)&&Array.isArray(t)?r.length!==t.length?!1:r.every((n,a)=>n===t[a]):r===t}l(x,"keyPathEquals");export function detectSchemaChanges(r,t){const n=[],a=[];for(const[o,u]of t){const s=r.get(o);if(!s){n.push({type:"store_add",storeName:o});continue}if(!x(s.keyPath,u.keyPath)){a.push({type:"keypath_change",storeName:o,oldKeyPath:s.keyPath,newKeyPath:u.keyPath});continue}for(const e of u.indexes){const i=s.indexes.get(e.name);if(!i)n.push({type:"index_add",storeName:o,indexName:e.name,index:e});else{const m=!x(i.keyPath,e.keyPath),h=i.unique!==(e.unique??!1),d=i.multiEntry!==(e.multiEntry??!1);(m||h||d)&&(n.push({type:"index_delete",storeName:o,indexName:e.name}),n.push({type:"index_add",storeName:o,indexName:e.name,index:e}))}}const c=new Set(u.indexes.map(e=>e.name));for(const e of s.indexes.keys())c.has(e)||n.push({type:"index_delete",storeName:o,indexName:e})}for(const o of r.keys())t.has(o)||a.push({type:"store_delete",storeName:o});return{safe:n,dangerous:a,hasChanges:n.length>0||a.length>0}}l(detectSchemaChanges,"detectSchemaChanges");export function applySafeChanges(r,t,n,a){const o=n.filter(s=>s.type==="store_rename"),u=new Map;for(const s of o){const c=t.objectStore(s.oldName),e=[],i=Array.from({length:c.indexNames.length},(h,d)=>c.indexNames.item(d));for(const h of i){const d=c.index(h);e.push({name:h,keyPath:d.keyPath,unique:d.unique,multiEntry:d.multiEntry})}u.set(s.oldName,{keyPath:c.keyPath,indexes:e,newName:s.newName});const m=c.getAll();m.onsuccess=()=>{const h=m.result,d=u.get(s.oldName),y=r.createObjectStore(d.newName,{keyPath:d.keyPath});for(const f of d.indexes)y.createIndex(f.name,f.keyPath,{unique:f.unique,multiEntry:f.multiEntry});for(const f of h)y.put(f)},r.deleteObjectStore(s.oldName)}for(const s of n)switch(s.type){case"store_add":{const c=a.find(e=>e.name===s.storeName);if(c){const e=r.createObjectStore(c.name,{keyPath:c.keyPath});for(const i of c.indexes)e.createIndex(i.name,i.keyPath,{unique:i.unique??!1,multiEntry:i.multiEntry??!1})}break}case"index_add":{t.objectStore(s.storeName).createIndex(s.indexName,s.index.keyPath,{unique:s.index.unique??!1,multiEntry:s.index.multiEntry??!1});break}case"index_delete":{t.objectStore(s.storeName).deleteIndex(s.indexName);break}}}l(applySafeChanges,"applySafeChanges");export async function getCurrentDatabaseVersion(r){return new Promise(t=>{const n=indexedDB.open(r);n.onsuccess=()=>{const a=n.result,o=a.version;a.close(),t(o)},n.onerror=()=>{t(0)}})}l(getCurrentDatabaseVersion,"getCurrentDatabaseVersion");export async function openDatabaseForSchemaRead(r){return new Promise(t=>{const n=indexedDB.open(r);n.onsuccess=()=>{t(n.result)},n.onerror=()=>{t(null)},n.onupgradeneeded=()=>{n.transaction?.abort(),t(null)}})}l(openDatabaseForSchemaRead,"openDatabaseForSchemaRead");export async function determineAutoVersion(r,t,n={}){const{removedStoreStrategy:a="error"}=n,o=await openDatabaseForSchemaRead(r);if(!o)return{version:1,changes:null,needsUpgrade:!0};const u=o.version,s=readExistingSchema(o),c=toDesiredSchema(t);o.close();const e=detectSchemaChanges(s,c);if(!e.hasChanges)return{version:u,changes:null,needsUpgrade:!1};const i=[];for(const m of e.dangerous)m.type==="store_delete"&&a==="preserve"?e.safe.push({type:"store_rename",oldName:m.storeName,newName:`__${m.storeName}_deleted_v${u}__`}):i.push(m);if(e.dangerous=i,e.dangerous.length>0){const h=`Dangerous schema changes detected:
2
+ ${e.dangerous.map(d=>{switch(d.type){case"store_delete":return`Store "${d.storeName}" would be deleted. Use removedStoreStrategy: 'preserve' to backup, or add a migration to explicitly delete it.`;case"keypath_change":return`Store "${d.storeName}" keyPath changed from "${d.oldKeyPath}" to "${d.newKeyPath}". This requires recreating the store with a manual migration.`;default:return"Unknown dangerous change"}}).join(`
3
3
  `)}
4
4
 
5
- Add explicit migrations to handle these changes safely.`)}return{version:a+1,changes:i,needsUpgrade:!0}}c(determineAutoVersion,"determineAutoVersion");
5
+ Add explicit migrations to handle these changes safely.`;throw console.error("[schema-idb]",h),new Error(h)}return{version:u+1,changes:e,needsUpgrade:!0}}l(determineAutoVersion,"determineAutoVersion");
@@ -1 +1 @@
1
- "use strict";var p=Object.defineProperty;var a=(r,t)=>p(r,"name",{value:t,configurable:!0});import{promisifyTransaction as i}from"./utils.js";import{createQueryFunction as S}from"./query.js";async function u(r,t){return await i(r),t.result}a(u,"getResult");function f(r,t){if(r!==void 0)return{...t,...r}}a(f,"applyDefaults");function w(r,t){return Object.keys(t).length===0?r:r.map(s=>({...t,...s}))}a(w,"applyDefaultsToArray");export function createStoreAccessor(r,t,s={}){const y=Object.keys(s).length>0,x=S(r,t,s);return{async get(c){const n=r.transaction(t,"readonly"),e=n.objectStore(t),o=await u(n,e.get(c));return y?f(o,s):o},async getAll(c){const n=r.transaction(t,"readonly"),e=n.objectStore(t),o=await u(n,e.getAll(c?.query,c?.count));return y?w(o,s):o},async getAllByIndex(c,n){const e=r.transaction(t,"readonly"),j=e.objectStore(t).index(c),l=await u(e,j.getAll(n));return y?w(l,s):l},async put(c,n){const e=r.transaction(t,"readwrite"),o=e.objectStore(t);return u(e,o.put(c,n))},async add(c,n){const e=r.transaction(t,"readwrite"),o=e.objectStore(t);return u(e,o.add(c,n))},async delete(c){const n=r.transaction(t,"readwrite");n.objectStore(t).delete(c),await i(n)},async clear(){const c=r.transaction(t,"readwrite");c.objectStore(t).clear(),await i(c)},async count(c){const n=r.transaction(t,"readonly"),o=n.objectStore(t).count(c);return await i(n),o.result},async raw(c){const n=r.transaction(t,"readwrite"),e=n.objectStore(t),o=c(e);return await i(n),o.result},query:x}}a(createStoreAccessor,"createStoreAccessor");
1
+ "use strict";var S=Object.defineProperty;var u=(r,t)=>S(r,"name",{value:t,configurable:!0});import{promisifyTransaction as y}from"./utils.js";import{createQueryFunction as p}from"./query.js";async function a(r,t){return await y(r),t.result}u(a,"getResult");function x(r,t){if(r!==void 0)return{...t,...r}}u(x,"applyDefaults");function j(r,t){return Object.keys(t).length===0?r:r.map(s=>({...t,...s}))}u(j,"applyDefaultsToArray");export function createStoreAccessor(r,t,s={}){const l=Object.keys(s).length>0,g=p(r,t,s);return{async get(c){const n=r.transaction(t,"readonly"),o=n.objectStore(t),e=await a(n,o.get(c));return l?x(e,s):e},async getAll(c){const n=r.transaction(t,"readonly"),o=n.objectStore(t),e=await a(n,o.getAll(c?.query,c?.count));return l?j(e,s):e},async getBy(c,n){const o=r.transaction(t,"readonly"),w=o.objectStore(t).index(c),i=await a(o,w.get(n));return l?x(i,s):i},async getAllBy(c,n){const o=r.transaction(t,"readonly"),w=o.objectStore(t).index(c),i=await a(o,w.getAll(n));return l?j(i,s):i},async put(c,n){const o=r.transaction(t,"readwrite"),e=o.objectStore(t);return a(o,e.put(c,n))},async add(c,n){const o=r.transaction(t,"readwrite"),e=o.objectStore(t);return a(o,e.add(c,n))},async delete(c){const n=r.transaction(t,"readwrite");n.objectStore(t).delete(c),await y(n)},async clear(){const c=r.transaction(t,"readwrite");c.objectStore(t).clear(),await y(c)},async count(c){const n=r.transaction(t,"readonly"),e=n.objectStore(t).count(c);return await y(n),e.result},async raw(c){const n=r.transaction(t,"readwrite"),o=n.objectStore(t),e=c(o);return await y(n),e.result},query:g}}u(createStoreAccessor,"createStoreAccessor");
package/dist/types.d.ts CHANGED
@@ -83,7 +83,8 @@ export interface WriteChain<TStores extends readonly StoreDefinition[]> {
83
83
  export interface StoreAccessor<T, K> {
84
84
  get(key: K): Promise<T | undefined>;
85
85
  getAll(options?: GetAllOptions): Promise<T[]>;
86
- getAllByIndex(indexName: string, query?: IDBKeyRange | IDBValidKey): Promise<T[]>;
86
+ getBy(indexName: string, query: IDBKeyRange | IDBValidKey): Promise<T | undefined>;
87
+ getAllBy(indexName: string, query?: IDBKeyRange | IDBValidKey): Promise<T[]>;
87
88
  put(value: T, key?: K): Promise<K>;
88
89
  add(value: T, key?: K): Promise<K>;
89
90
  delete(key: K | IDBKeyRange): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-idb",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Type-safe IndexedDB wrapper with chainable transactions",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -1,2 +0,0 @@
1
- import type { StoreDefinition, DatabaseConfig, DatabaseWithStores } from './types.js';
2
- export declare function openDB<const TStores extends readonly StoreDefinition[]>(config: DatabaseConfig<TStores>): Promise<DatabaseWithStores<TStores>>;
package/dist/createDB.js DELETED
@@ -1 +0,0 @@
1
- "use strict";var w=Object.defineProperty;var f=(n,t)=>w(n,"name",{value:t,configurable:!0});import{openDatabase as y,promisifyTransaction as b}from"./utils.js";import{createReadChain as S}from"./readChain.js";import{createWriteChain as v}from"./writeChain.js";import{createStoreAccessor as x}from"./storeAccessor.js";function D(n){const t=[],s=new Set;for(const r of n)for(const e of r.migrations){if(s.has(e.name))throw new Error(`Duplicate migration name "${e.name}" found across stores`);s.add(e.name),t.push(e)}return t.sort((r,e)=>r.name.localeCompare(e.name))}f(D,"collectMigrations");function E(n,t,s){const r={...n};for(const e of s)r[e.name]=x(t,e.name);return r}f(E,"buildDatabaseWithStores");function P(n,t,s,r,e){if(s===0)for(const a of r){const i=n.createObjectStore(a.name,{keyPath:a.keyPath,autoIncrement:a.autoIncrement});for(const c of a.indexes)i.createIndex(c.name,c.keyPath,{unique:c.unique??!1,multiEntry:c.multiEntry??!1})}for(const a of e)try{const i=a.up(n,t);i instanceof Promise&&i.catch(c=>{console.error(`Migration "${a.name}" failed:`,c),t.abort()})}catch(i){throw console.error(`Migration "${a.name}" failed:`,i),t.abort(),i}}f(P,"handleUpgrade");export async function openDB(n){const{name:t,version:s,versionStrategy:r="explicit",stores:e,onBlocked:a,onVersionChange:i}=n,c=new Set;for(const o of e){if(c.has(o.name))throw new Error(`Duplicate store name: "${o.name}"`);c.add(o.name)}let u;if(r==="auto")u=1;else{if(s===void 0)throw new Error('Version is required when versionStrategy is "explicit"');u=s}const p=D(e),m=await y(t,u,(o,l,d)=>{P(o,l,d,e,p)},a);return i&&(m.onversionchange=i),E({get name(){return m.name},get version(){return m.version},get raw(){return m},close(){m.close()},read(o){return S(m,o)},write(o){return v(m,o)},async transaction(o,l,d){const h=m.transaction(o,l),g=d(h);g instanceof Promise&&await g,await b(h)}},m,e)}f(openDB,"openDB");
@@ -1,5 +0,0 @@
1
- import type { StoreDefinition, StoreKeyPath, ExtractKeyType, StoreOptionsWithKeyPath, StoreOptionsWithoutKeyPath } from './types.js';
2
- export declare function defineStore<T>(): {
3
- <const TName extends string, const KP extends StoreKeyPath<T>>(name: TName, options: StoreOptionsWithKeyPath<T, KP>): StoreDefinition<T, ExtractKeyType<T, KP>, TName>;
4
- <const TName extends string, K extends IDBValidKey = IDBValidKey>(name: TName, options?: StoreOptionsWithoutKeyPath<T, K>): StoreDefinition<T, K, TName>;
5
- };
@@ -1 +0,0 @@
1
- "use strict";var p=Object.defineProperty;var i=(t,e)=>p(t,"name",{value:e,configurable:!0});function g(t){if(!t)return[];const e=[];for(const[r,o]of Object.entries(t))o!==!1&&(o===!0?e.push({name:r,keyPath:r}):e.push({name:r,keyPath:r,unique:o.unique,multiEntry:o.multiEntry}));return e}i(g,"parseIndexConfig");export function defineStore(){function t(e,r={}){const{keyPath:o,autoIncrement:u=!1,indexes:m,migrations:a=[]}=r,f=g(m);if(!e||typeof e!="string")throw new Error("Store name is required and must be a string");const s=new Set;for(const n of a){if(!n.name||typeof n.name!="string")throw new Error(`Invalid migration name in store "${e}": must be a non-empty string`);if(s.has(n.name))throw new Error(`Duplicate migration name "${n.name}" in store "${e}"`);s.add(n.name)}return{name:e,keyPath:o,autoIncrement:u,indexes:f,migrations:[...a].sort((n,c)=>n.name.localeCompare(c.name)),_schema:{},_keyType:{}}}return i(t,"createStore"),t}i(defineStore,"defineStore");
@@ -1,2 +0,0 @@
1
- import type { ReadChain, StoreDefinition } from './types.js';
2
- export declare function createReadChain<TStores extends readonly StoreDefinition[]>(db: IDBDatabase, storeNames: string[]): ReadChain<TStores, []>;
package/dist/readChain.js DELETED
@@ -1 +0,0 @@
1
- "use strict";var l=Object.defineProperty;var i=(s,u)=>l(s,"name",{value:u,configurable:!0});import{promisifyTransaction as p}from"./utils.js";function g(s){return s}i(g,"toReadChain");export function createReadChain(s,u){const r=[],c={get(n,t){return r.push({type:"get",storeName:n,key:t}),c},getAll(n,t){return r.push({type:"getAll",storeName:n,query:t?.query,count:t?.count}),c},getAllByIndex(n,t,o){return r.push({type:"getAllByIndex",storeName:n,indexName:t,query:o}),c},count(n,t){return r.push({type:"count",storeName:n,query:t}),c},async execute(){if(r.length===0)return[];const n=[...new Set(r.map(e=>e.storeName))];for(const e of n)if(!u.includes(e))throw new Error(`Store "${e}" is not in the transaction scope. Available stores: ${u.join(", ")}`);const t=s.transaction(u,"readonly"),o=[];for(const e of r){const a=t.objectStore(e.storeName);switch(e.type){case"get":o.push(a.get(e.key));break;case"getAll":o.push(a.getAll(e.query,e.count));break;case"getAllByIndex":o.push(a.index(e.indexName).getAll(e.query));break;case"count":o.push(a.count(e.query));break}}return await p(t),o.map(e=>e.result)}};return c}i(createReadChain,"createReadChain");
@@ -1,2 +0,0 @@
1
- import type { WriteChain, StoreDefinition } from './types.js';
2
- export declare function createWriteChain<TStores extends readonly StoreDefinition[]>(db: IDBDatabase, storeNames: string[]): WriteChain<TStores>;
@@ -1 +0,0 @@
1
- "use strict";var u=Object.defineProperty;var i=(n,o)=>u(n,"name",{value:o,configurable:!0});import{promisifyTransaction as p}from"./utils.js";function h(n){return n}i(h,"toWriteChain");export function createWriteChain(n,o){const a=[],s={put(t,r,e){return a.push({type:"put",storeName:t,value:r,key:e}),s},add(t,r,e){return a.push({type:"add",storeName:t,value:r,key:e}),s},delete(t,r){return a.push({type:"delete",storeName:t,key:r}),s},clear(t){return a.push({type:"clear",storeName:t}),s},async execute(){if(a.length===0)return;const t=[...new Set(a.map(e=>e.storeName))];for(const e of t)if(!o.includes(e))throw new Error(`Store "${e}" is not in the transaction scope. Available stores: ${o.join(", ")}`);const r=n.transaction(o,"readwrite");for(const e of a){const c=r.objectStore(e.storeName);switch(e.type){case"put":c.put(e.value,e.key);break;case"add":c.add(e.value,e.key);break;case"delete":c.delete(e.key);break;case"clear":c.clear();break}}await p(r)}};return s}i(createWriteChain,"createWriteChain");