@bjnstnkvc/db 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,13 +94,13 @@ await DB.migrate('app');
94
94
  and every example below passes it as `DB.table<User>('users')` so the builder can type its
95
95
  constraints, its return values and its aggregate keys.
96
96
 
97
- | Option | Meaning |
98
- | --- | --- |
99
- | `default` | The connection used when none is named |
100
- | `connections[name].database` | The IndexedDB database name |
101
- | `connections[name].migrations` | Ordered migration classes. Their order **is** the schema version. |
102
- | `connections[name].seeders` | Ordered seeder classes, run by `DB.seed(name)`. See [Seeding](#seeding). |
103
- | `connections[name].strict` | Defaults to `true`. Nullability violations and uncoercible values throw. `false` writes `null` instead. |
97
+ | Option | Meaning |
98
+ |--------------------------------|---------------------------------------------------------------------------------------------------------|
99
+ | `default` | The connection used when none is named |
100
+ | `connections[name].database` | The IndexedDB database name |
101
+ | `connections[name].migrations` | Ordered migration classes. Their order **is** the schema version. |
102
+ | `connections[name].seeders` | Ordered seeder classes, run by `DB.seed(name)`. See [Seeding](#seeding). |
103
+ | `connections[name].strict` | Defaults to `true`. Nullability violations and uncoercible values throw. `false` writes `null` instead. |
104
104
 
105
105
  `DB.migrate(name)` is idempotent. It opens the database at the version your migrations ask for, and
106
106
  when that already matches, nothing runs. Calling it on every boot is the intended usage, and there
@@ -144,8 +144,10 @@ start over, `DB.fresh(name)` deletes the database and replays every migration.
144
144
  Migrations may only ever be **appended**. Reordering them, or removing one that already ran, throws
145
145
  `MigrationMismatchException` rather than corrupting the schema.
146
146
 
147
- The recorded name defaults to the class name, so a bundler that mangles class names will look like a
148
- reordered list. If you minify with class-name mangling, override `name()`:
147
+ The recorded name defaults to the class name in snake case, the way Laravel names a migration file,
148
+ so `CreateUsersTable` is recorded as `create_users_table`. Because it is derived from the class name,
149
+ a bundler that mangles class names will look like a reordered list. If you minify with class-name
150
+ mangling, override `name()`:
149
151
 
150
152
  ```ts
151
153
  class CreateUsersTable extends Migration {
@@ -184,15 +186,15 @@ Resolves to one entry per registered migration:
184
186
 
185
187
  ```
186
188
  [
187
- { migration: 'CreateUsersTable', ran: true, at: '2026-08-27T21:00:00.000Z' },
188
- { migration: 'AddRoleToUsersTable', ran: false, at: null }
189
+ { migration: 'create_users_table', ran: true, at: '2026-08-27T21:00:00.000Z' },
190
+ { migration: 'add_role_to_users_table', ran: false, at: null }
189
191
  ]
190
192
  ```
191
193
 
192
194
  `DB.status(name)` never migrates as a side effect, so you can call it before `DB.migrate(name)` to
193
195
  see what is pending.
194
196
 
195
- > modeled on Laravel's [Migrations](https://laravel.com/docs/12.x/migrations). These only run
197
+ > Modeled on Laravel's [Migrations](https://laravel.com/docs/12.x/migrations). These only run
196
198
  > forward, and they are registered in the connection config rather than discovered from a directory.
197
199
 
198
200
  ### Seeding
@@ -478,7 +480,7 @@ await DB.fresh('app');
478
480
  await DB.fresh('app', { seed: true });
479
481
  ```
480
482
 
481
- > modeled on Laravel's [Database: Seeding](https://laravel.com/docs/12.x/seeding), down to the
483
+ > Modeled on Laravel's [Database: Seeding](https://laravel.com/docs/12.x/seeding), down to the
482
484
  > seeded connection standing in as the default for the duration of the run.
483
485
 
484
486
  ### Defining a schema
@@ -486,21 +488,21 @@ await DB.fresh('app', { seed: true });
486
488
  IndexedDB stores whole objects and enforces only a key path, `autoIncrement` and indexes. Column
487
489
  types are recorded as metadata and enforced by this package at write time.
488
490
 
489
- | Blueprint | Effect |
490
- | --- | --- |
491
- | `table.id()` | `keyPath: 'id'`, `autoIncrement: true` |
492
- | `table.uuid('id').primary()` | `keyPath: 'id'`, no autoIncrement |
493
- | `table.string` / `integer` / `float` / `boolean` / `date` / `datetime` / `json` | Column metadata |
494
- | `table.decimal('price', 2)` | Column metadata, stored as a whole number of the smallest unit |
495
- | `table.enum('role', Role)` | Column metadata, checked at write time. Takes a list, an enum or a constant object |
496
- | `.nullable()` | Metadata, enforced at write time |
497
- | `.default(value)` | Applied at write time, and backfilled when added to an existing table |
498
- | `.primary()` | Makes the column the key path. At most one per table. |
499
- | `.index()` | `createIndex('users_name_index', 'name')` |
500
- | `.unique()` | `createIndex('users_email_unique', 'email', { unique: true })` |
501
- | `table.index(['a', 'b'])` | Compound index |
502
- | `.multiEntry()` | One index entry per array element |
503
- | `table.timestamps()` | Nullable `created_at` / `updated_at`, filled automatically |
491
+ | Blueprint | Effect |
492
+ |---------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
493
+ | `table.id()` | `keyPath: 'id'`, `autoIncrement: true` |
494
+ | `table.uuid('id').primary()` | `keyPath: 'id'`, no autoIncrement |
495
+ | `table.string` / `integer` / `float` / `boolean` / `date` / `datetime` / `json` | Column metadata |
496
+ | `table.decimal('price', 2)` | Column metadata, stored as a whole number of the smallest unit |
497
+ | `table.enum('role', Role)` | Column metadata, checked at write time. Takes a list, an enum or a constant object |
498
+ | `.nullable()` | Metadata, enforced at write time |
499
+ | `.default(value)` | Applied at write time, and backfilled when added to an existing table |
500
+ | `.primary()` | Makes the column the key path. At most one per table. |
501
+ | `.index()` | `createIndex('users_name_index', 'name')` |
502
+ | `.unique()` | `createIndex('users_email_unique', 'email', { unique: true })` |
503
+ | `table.index(['a', 'b'])` | Compound index |
504
+ | `.multiEntry()` | One index entry per array element |
505
+ | `table.timestamps()` | Nullable `created_at` / `updated_at`, filled automatically |
504
506
 
505
507
  Altering a table also supports `dropColumn`, `renameColumn`, `dropIndex` and `Schema.rename`. The key
506
508
  path may not be dropped or renamed, because IndexedDB fixes it when the store is created.
@@ -665,7 +667,7 @@ await Schema.getIndexes('users');
665
667
  await Schema.connection('reporting').hasTable('reports');
666
668
  ```
667
669
 
668
- > modeled on Laravel's [Migrations: Tables](https://laravel.com/docs/12.x/migrations#tables).
670
+ > Modeled on Laravel's [Migrations: Tables](https://laravel.com/docs/12.x/migrations#tables).
669
671
  > Column types are metadata this package enforces at write time, since IndexedDB stores whole objects
670
672
  > and checks nothing itself.
671
673
 
@@ -771,22 +773,22 @@ await DB.table<User>('users').min('age');
771
773
  await DB.table<User>('users').max('age');
772
774
  ```
773
775
 
774
- | Terminal | Resolves to |
775
- | --- | --- |
776
- | `get()` | `T[]` |
777
- | `first()` | `T` or `null` |
778
- | `firstOrFail()` | `T`, or throws `RecordsNotFoundException` |
779
- | `find(key)` | `T` or `null`, by point lookup on the key path |
780
- | `findOrFail(key)` | `T`, or throws `RecordsNotFoundException` |
781
- | `value(column)` | The column of the first matching record, or `null` |
782
- | `pluck(column)` | `V[]` in result order |
783
- | `pluck(column, key)` | `Record<string, V>`, keyed by a second column |
784
- | `exists()` / `doesntExist()` | `boolean` |
785
- | `count()` | `number` |
786
- | `sum(column)` | `number` |
787
- | `avg(column)` / `min(column)` / `max(column)` | `number` or `null` when nothing matched |
788
- | `sole()` | `T`, or throws `RecordsNotFoundException` / `MultipleRecordsFoundException` |
789
- | `paginate(page?, perPage?)` | `{ data, total, perPage, currentPage, lastPage }` |
776
+ | Terminal | Resolves to |
777
+ |-----------------------------------------------|-----------------------------------------------------------------------------|
778
+ | `get()` | `T[]` |
779
+ | `first()` | `T` or `null` |
780
+ | `firstOrFail()` | `T`, or throws `RecordsNotFoundException` |
781
+ | `find(key)` | `T` or `null`, by point lookup on the key path |
782
+ | `findOrFail(key)` | `T`, or throws `RecordsNotFoundException` |
783
+ | `value(column)` | The column of the first matching record, or `null` |
784
+ | `pluck(column)` | `V[]` in result order |
785
+ | `pluck(column, key)` | `Record<string, V>`, keyed by a second column |
786
+ | `exists()` / `doesntExist()` | `boolean` |
787
+ | `count()` | `number` |
788
+ | `sum(column)` | `number` |
789
+ | `avg(column)` / `min(column)` / `max(column)` | `number` or `null` when nothing matched |
790
+ | `sole()` | `T`, or throws `RecordsNotFoundException` / `MultipleRecordsFoundException` |
791
+ | `paginate(page?, perPage?)` | `{ data, total, perPage, currentPage, lastPage }` |
790
792
 
791
793
  `min` and `max` read the answer straight off the index when the column has one and the query is
792
794
  unconstrained, so they cost one cursor rather than a full scan.
@@ -852,7 +854,7 @@ The key path may not be updated, so `update`, `upsert` and `increment` all refus
852
854
  when an index drives the query and key order otherwise. Pair them with an indexed `orderBy` if you
853
855
  need a defined order.
854
856
 
855
- > modeled on Laravel's [Database: Query Builder](https://laravel.com/docs/12.x/queries). The method
857
+ > Modeled on Laravel's [Database: Query Builder](https://laravel.com/docs/12.x/queries). The method
856
858
  > names and their semantics match, and every terminal is asynchronous because IndexedDB is.
857
859
 
858
860
  ### Joins
@@ -959,7 +961,7 @@ covers it, and `chunk` slices the materialised result rather than walking keys.
959
961
  await DB.table('users').whereColumn('updated_at', '>', 'created_at').get();
960
962
  ```
961
963
 
962
- > modeled on Laravel's [Query Builder: Joins](https://laravel.com/docs/12.x/queries#joins). Rows
964
+ > Modeled on Laravel's [Query Builder: Joins](https://laravel.com/docs/12.x/queries#joins). Rows
963
965
  > stay flat as they do in Laravel, and the join itself runs in memory because IndexedDB has none.
964
966
 
965
967
  ### Grouping
@@ -994,13 +996,13 @@ Because the alias is an object key rather than a string inside an expression, th
994
996
  inferred rather than cast. That row is typed `{ role: string; total: number; oldest: number | null }`,
995
997
  and reading a column you did not group or aggregate is a compile error.
996
998
 
997
- | Aggregate | Meaning |
998
- | --- | --- |
999
- | `{ count: '*' }` | The number of records in the group, always a `number` |
1000
- | `{ count: 'column' }` | The number of records whose column is not null |
1001
- | `{ sum: 'column' }` | The total, `0` for a group with no values |
1002
- | `{ avg: 'column' }` | The mean, `null` for a group with no values |
1003
- | `{ min: 'column' }` / `{ max: 'column' }` | The extreme, `null` for a group with no values |
999
+ | Aggregate | Meaning |
1000
+ |-------------------------------------------|-------------------------------------------------------|
1001
+ | `{ count: '*' }` | The number of records in the group, always a `number` |
1002
+ | `{ count: 'column' }` | The number of records whose column is not null |
1003
+ | `{ sum: 'column' }` | The total, `0` for a group with no values |
1004
+ | `{ avg: 'column' }` | The mean, `null` for a group with no values |
1005
+ | `{ min: 'column' }` / `{ max: 'column' }` | The extreme, `null` for a group with no values |
1004
1006
 
1005
1007
  Group by several columns by passing several names:
1006
1008
 
@@ -1032,7 +1034,7 @@ await DB.table<User>('users')
1032
1034
  Grouping happens in memory after the records are fetched, so the planner still applies to the
1033
1035
  `where` clauses that select them, and a grouped query reports the plan of that underlying fetch.
1034
1036
 
1035
- > modeled on Laravel's [Query Builder: Grouping](https://laravel.com/docs/12.x/queries#groupby-having),
1037
+ > Modeled on Laravel's [Query Builder: Grouping](https://laravel.com/docs/12.x/queries#groupby-having),
1036
1038
  > with the aggregates named in a typed object instead of raw SQL.
1037
1039
 
1038
1040
  ### Query plans
@@ -1092,7 +1094,7 @@ commits behind your back the first time you await anything outside it, so offeri
1092
1094
  offering a trap. The same rule as migrations applies here: the callback may only await operations
1093
1095
  from this package.
1094
1096
 
1095
- > modeled on Laravel's [Database: Transactions](https://laravel.com/docs/12.x/database#database-transactions).
1097
+ > Modeled on Laravel's [Database: Transactions](https://laravel.com/docs/12.x/database#database-transactions).
1096
1098
  > The tables have to be declared up front, because an IndexedDB transaction fixes its scope when it
1097
1099
  > opens.
1098
1100
 
@@ -1116,9 +1118,6 @@ Available events: `query`, `transaction-beginning`, `transaction-committed`,
1116
1118
  A connection with no seeders announces nothing, so `seeding-started` firing always means at least
1117
1119
  one seeder is about to run.
1118
1120
 
1119
- Listeners are **persistent by default**, with an opt-in `{ once: true }`. This is a deliberate
1120
- departure from `@bjnstnkvc/local-storage`, where every listener fires exactly once.
1121
-
1122
1121
  ```ts
1123
1122
  DB.enableQueryLog();
1124
1123
 
@@ -1148,7 +1147,7 @@ DB.disableQueryLog();
1148
1147
 
1149
1148
  `DB.logging()` then returns `false`, and `DB.getQueryLog()` an empty array.
1150
1149
 
1151
- > modeled on Laravel's [Database: Listening for Query Events](https://laravel.com/docs/12.x/database#listening-for-query-events),
1150
+ > Modeled on Laravel's [Database: Listening for Query Events](https://laravel.com/docs/12.x/database#listening-for-query-events),
1152
1151
  > with the same enable, get and flush surface, dispatched as a browser event.
1153
1152
 
1154
1153
  ### Multiple tabs
@@ -1171,14 +1170,14 @@ DB.disconnect('app');
1171
1170
  DB.purge('app');
1172
1171
  ```
1173
1172
 
1174
- | Call | Effect |
1175
- | --- | --- |
1176
- | `connection()` | The default connection |
1177
- | `connection(name)` | A named connection, cached after the first resolve |
1173
+ | Call | Effect |
1174
+ |--------------------|----------------------------------------------------------------------------------|
1175
+ | `connection()` | The default connection |
1176
+ | `connection(name)` | A named connection, cached after the first resolve |
1178
1177
  | `disconnect(name)` | Close the handle, leaving the connection registered so the next query reopens it |
1179
- | `purge(name)` | Close it and drop it, so the next resolve rebuilds it from configuration |
1178
+ | `purge(name)` | Close it and drop it, so the next resolve rebuilds it from configuration |
1180
1179
 
1181
- > modeled on Laravel's [Database: Multiple Connections](https://laravel.com/docs/12.x/database#using-multiple-database-connections),
1180
+ > Modeled on Laravel's [Database: Multiple Connections](https://laravel.com/docs/12.x/database#using-multiple-database-connections),
1182
1181
  > resolved by name and cached, with one IndexedDB database behind each.
1183
1182
 
1184
1183
  ### Storage quota
@@ -1233,21 +1232,21 @@ memory, so writes inside a narrowed transaction still get their defaults.
1233
1232
  Every exception extends `Error` and sets its own `name`, so `instanceof` and the stack both read
1234
1233
  true. All of them are exported from the package root.
1235
1234
 
1236
- | Exception | Thrown when |
1237
- | --- | --- |
1238
- | `CheckConstraintViolationException` | A write gives an enumerated column a value it does not accept |
1239
- | `ConnectionNotConfiguredException` | A connection is resolved under a name `DB.configure` never declared |
1240
- | `DatabaseBlockedException` | Another tab holds the database open at an older version, so the upgrade cannot start |
1241
- | `MigrationMismatchException` | The recorded migration list is not a prefix of the registered one, so one was removed, renamed or reordered |
1242
- | `MigrationTransactionClosedException` | A migration awaited something outside this package, letting the versionchange transaction commit early |
1243
- | `MultipleRecordsFoundException` | `sole()` matched more than one record |
1244
- | `NotNullConstraintViolationException` | A non-nullable column is written as null, or is absent with no default |
1245
- | `QuotaExceededException` | The origin's storage quota stopped the operation |
1246
- | `RecordsNotFoundException` | `firstOrFail()`, `sole()` or `findOrFail()` matched nothing |
1247
- | `ReservedTableException` | A migration tries to create `migrations` or `schema` |
1248
- | `SchemaException` | A schema or query call the shape of the database cannot support |
1249
- | `TableNotFoundException` | A query or schema read names a table the database does not have |
1250
- | `UniqueConstraintViolationException` | A write collides with a unique index, named in the message |
1235
+ | Exception | Thrown when |
1236
+ |---------------------------------------|-------------------------------------------------------------------------------------------------------------|
1237
+ | `CheckConstraintViolationException` | A write gives an enumerated column a value it does not accept |
1238
+ | `ConnectionNotConfiguredException` | A connection is resolved under a name `DB.configure` never declared |
1239
+ | `DatabaseBlockedException` | Another tab holds the database open at an older version, so the upgrade cannot start |
1240
+ | `MigrationMismatchException` | The recorded migration list is not a prefix of the registered one, so one was removed, renamed or reordered |
1241
+ | `MigrationTransactionClosedException` | A migration awaited something outside this package, letting the versionchange transaction commit early |
1242
+ | `MultipleRecordsFoundException` | `sole()` matched more than one record |
1243
+ | `NotNullConstraintViolationException` | A non-nullable column is written as null, or is absent with no default |
1244
+ | `QuotaExceededException` | The origin's storage quota stopped the operation |
1245
+ | `RecordsNotFoundException` | `firstOrFail()`, `sole()` or `findOrFail()` matched nothing |
1246
+ | `ReservedTableException` | A migration tries to create `migrations` or `schema` |
1247
+ | `SchemaException` | A schema or query call the shape of the database cannot support |
1248
+ | `TableNotFoundException` | A query or schema read names a table the database does not have |
1249
+ | `UniqueConstraintViolationException` | A write collides with a unique index, named in the message |
1251
1250
 
1252
1251
  `SchemaException` is the broad one, so here is every case that raises it:
1253
1252
 
package/dist/main.cjs CHANGED
@@ -4141,7 +4141,7 @@ var Migration = class {
4141
4141
  * Get the name of the migration.
4142
4142
  */
4143
4143
  name() {
4144
- return this.constructor.name;
4144
+ return this.constructor.name.replace(/([a-z\d])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").toLowerCase();
4145
4145
  }
4146
4146
  };
4147
4147
 
package/dist/main.js CHANGED
@@ -4077,7 +4077,7 @@ var Migration = class {
4077
4077
  * Get the name of the migration.
4078
4078
  */
4079
4079
  name() {
4080
- return this.constructor.name;
4080
+ return this.constructor.name.replace(/([a-z\d])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").toLowerCase();
4081
4081
  }
4082
4082
  };
4083
4083
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bjnstnkvc/db",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "TypeScript database layer for IndexedDB with an API modeled on Laravel.",
5
5
  "type": "module",
6
6
  "main": "./dist/main.cjs",