@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 +80 -81
- package/dist/main.cjs +1 -1
- package/dist/main.js +1 -1
- package/package.json +1 -1
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
|
|
98
|
-
|
|
99
|
-
| `default`
|
|
100
|
-
| `connections[name].database`
|
|
101
|
-
| `connections[name].migrations` | Ordered migration classes. Their order **is** the schema version.
|
|
102
|
-
| `connections[name].seeders`
|
|
103
|
-
| `connections[name].strict`
|
|
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
|
|
148
|
-
|
|
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: '
|
|
188
|
-
{ migration: '
|
|
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
|
-
>
|
|
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
|
-
>
|
|
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
|
|
490
|
-
|
|
491
|
-
| `table.id()`
|
|
492
|
-
| `table.uuid('id').primary()`
|
|
493
|
-
| `table.string` / `integer` / `float` / `boolean` / `date` / `datetime` / `json` | Column metadata
|
|
494
|
-
| `table.decimal('price', 2)`
|
|
495
|
-
| `table.enum('role', Role)`
|
|
496
|
-
| `.nullable()`
|
|
497
|
-
| `.default(value)`
|
|
498
|
-
| `.primary()`
|
|
499
|
-
| `.index()`
|
|
500
|
-
| `.unique()`
|
|
501
|
-
| `table.index(['a', 'b'])`
|
|
502
|
-
| `.multiEntry()`
|
|
503
|
-
| `table.timestamps()`
|
|
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
|
-
>
|
|
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
|
|
775
|
-
|
|
776
|
-
| `get()`
|
|
777
|
-
| `first()`
|
|
778
|
-
| `firstOrFail()`
|
|
779
|
-
| `find(key)`
|
|
780
|
-
| `findOrFail(key)`
|
|
781
|
-
| `value(column)`
|
|
782
|
-
| `pluck(column)`
|
|
783
|
-
| `pluck(column, key)`
|
|
784
|
-
| `exists()` / `doesntExist()`
|
|
785
|
-
| `count()`
|
|
786
|
-
| `sum(column)`
|
|
787
|
-
| `avg(column)` / `min(column)` / `max(column)` | `number` or `null` when nothing matched
|
|
788
|
-
| `sole()`
|
|
789
|
-
| `paginate(page?, perPage?)`
|
|
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
|
-
>
|
|
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
|
-
>
|
|
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
|
|
998
|
-
|
|
999
|
-
| `{ count: '*' }`
|
|
1000
|
-
| `{ count: 'column' }`
|
|
1001
|
-
| `{ sum: 'column' }`
|
|
1002
|
-
| `{ avg: 'column' }`
|
|
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
|
-
>
|
|
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
|
-
>
|
|
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
|
-
>
|
|
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
|
|
1175
|
-
|
|
1176
|
-
| `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)`
|
|
1178
|
+
| `purge(name)` | Close it and drop it, so the next resolve rebuilds it from configuration |
|
|
1180
1179
|
|
|
1181
|
-
>
|
|
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
|
|
1237
|
-
|
|
1238
|
-
| `CheckConstraintViolationException`
|
|
1239
|
-
| `ConnectionNotConfiguredException`
|
|
1240
|
-
| `DatabaseBlockedException`
|
|
1241
|
-
| `MigrationMismatchException`
|
|
1242
|
-
| `MigrationTransactionClosedException` | A migration awaited something outside this package, letting the versionchange transaction commit early
|
|
1243
|
-
| `MultipleRecordsFoundException`
|
|
1244
|
-
| `NotNullConstraintViolationException` | A non-nullable column is written as null, or is absent with no default
|
|
1245
|
-
| `QuotaExceededException`
|
|
1246
|
-
| `RecordsNotFoundException`
|
|
1247
|
-
| `ReservedTableException`
|
|
1248
|
-
| `SchemaException`
|
|
1249
|
-
| `TableNotFoundException`
|
|
1250
|
-
| `UniqueConstraintViolationException`
|
|
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
|
|