schema-idb 0.0.3 → 0.0.5

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