@bunnykit/orm 0.1.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +904 -0
  3. package/dist/bin/bunny.d.ts +2 -0
  4. package/dist/bin/bunny.js +108 -0
  5. package/dist/src/connection/Connection.d.ts +13 -0
  6. package/dist/src/connection/Connection.js +49 -0
  7. package/dist/src/index.d.ts +20 -0
  8. package/dist/src/index.js +18 -0
  9. package/dist/src/migration/Migration.d.ts +4 -0
  10. package/dist/src/migration/Migration.js +2 -0
  11. package/dist/src/migration/MigrationCreator.d.ts +5 -0
  12. package/dist/src/migration/MigrationCreator.js +39 -0
  13. package/dist/src/migration/Migrator.d.ts +21 -0
  14. package/dist/src/migration/Migrator.js +137 -0
  15. package/dist/src/model/BelongsToMany.d.ts +27 -0
  16. package/dist/src/model/BelongsToMany.js +118 -0
  17. package/dist/src/model/Model.d.ts +166 -0
  18. package/dist/src/model/Model.js +763 -0
  19. package/dist/src/model/MorphMap.d.ts +7 -0
  20. package/dist/src/model/MorphMap.js +12 -0
  21. package/dist/src/model/MorphRelations.d.ts +81 -0
  22. package/dist/src/model/MorphRelations.js +296 -0
  23. package/dist/src/model/Observer.d.ts +19 -0
  24. package/dist/src/model/Observer.js +21 -0
  25. package/dist/src/query/Builder.d.ts +106 -0
  26. package/dist/src/query/Builder.js +466 -0
  27. package/dist/src/schema/Blueprint.d.ts +66 -0
  28. package/dist/src/schema/Blueprint.js +200 -0
  29. package/dist/src/schema/Schema.d.ts +16 -0
  30. package/dist/src/schema/Schema.js +135 -0
  31. package/dist/src/schema/grammars/Grammar.d.ts +26 -0
  32. package/dist/src/schema/grammars/Grammar.js +95 -0
  33. package/dist/src/schema/grammars/MySqlGrammar.d.ts +18 -0
  34. package/dist/src/schema/grammars/MySqlGrammar.js +96 -0
  35. package/dist/src/schema/grammars/PostgresGrammar.d.ts +16 -0
  36. package/dist/src/schema/grammars/PostgresGrammar.js +88 -0
  37. package/dist/src/schema/grammars/SQLiteGrammar.d.ts +16 -0
  38. package/dist/src/schema/grammars/SQLiteGrammar.js +108 -0
  39. package/dist/src/typegen/TypeGenerator.d.ts +29 -0
  40. package/dist/src/typegen/TypeGenerator.js +171 -0
  41. package/dist/src/typegen/TypeMapper.d.ts +4 -0
  42. package/dist/src/typegen/TypeMapper.js +27 -0
  43. package/dist/src/types/index.d.ts +53 -0
  44. package/dist/src/types/index.js +1 -0
  45. package/dist/src/utils.d.ts +1 -0
  46. package/dist/src/utils.js +6 -0
  47. package/package.json +62 -0
package/README.md ADDED
@@ -0,0 +1,904 @@
1
+ # Bunny
2
+
3
+ An **Eloquent-inspired ORM** built specifically for [Bun](https://bun.sh)'s native `bun:sql` client. Supports **SQLite**, **MySQL**, and **PostgreSQL** with full TypeScript typing, a chainable query builder, schema migrations, model observers, and polymorphic relations.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ - 🔥 **Bun-native** — Built on top of `bun:sql` for maximum performance
10
+ - 📦 **Multi-database** — SQLite, MySQL, and PostgreSQL support
11
+ - 🔷 **Fully Typed** — Written in TypeScript with generics everywhere
12
+ - 🏗️ **Schema Builder** — Programmatic table creation, indexes, foreign keys
13
+ - 🔍 **Query Builder** — Chainable `where`, `join`, `orderBy`, `groupBy`, etc.
14
+ - 🧬 **Eloquent-style Models** — Property attributes, defaults, casts, dirty tracking, soft deletes, scopes
15
+ - 🔗 **Relations** — Standard, many-to-many, polymorphic, through, one-of-many, and relation queries
16
+ - 👁️ **Observers** — Lifecycle hooks (`creating`, `created`, `updating`, `updated`, etc.)
17
+ - 🚀 **Migrations & CLI** — Create, run, and rollback migrations from the command line
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ bun add @bunnykit/orm
25
+ ```
26
+
27
+ > **Note:** This package is Bun-only. Install and run it with Bun >= 1.1; npm, yarn, pnpm, and Node.js runtime usage are not supported.
28
+
29
+ ---
30
+
31
+ ## Configuration
32
+
33
+ Create a `bunny.config.ts` (or `.js`) in your project root:
34
+
35
+ ```ts
36
+ export default {
37
+ connection: {
38
+ // Option 1: connection string
39
+ url: "sqlite://app.db",
40
+
41
+ // Option 2: driver config (for MySQL / Postgres)
42
+ // driver: "mysql",
43
+ // host: "localhost",
44
+ // port: 3306,
45
+ // database: "mydb",
46
+ // username: "root",
47
+ // password: "secret",
48
+ },
49
+ migrationsPath: "./database/migrations",
50
+ };
51
+ ```
52
+
53
+ Or use environment variables:
54
+
55
+ ```bash
56
+ export DATABASE_URL="sqlite://app.db"
57
+ export MIGRATIONS_PATH="./database/migrations"
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Quick Start
63
+
64
+ ### Define a Model
65
+
66
+ ```ts
67
+ import { Model } from "@bunnykit/orm";
68
+
69
+ class User extends Model {
70
+ // Optional — inferred as "users" if omitted
71
+ static table = "users";
72
+
73
+ posts() {
74
+ return this.hasMany(Post);
75
+ }
76
+ }
77
+
78
+ class Post extends Model {
79
+ static table = "posts";
80
+
81
+ author() {
82
+ return this.belongsTo(User);
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### Set the Database Connection
88
+
89
+ ```ts
90
+ import { Connection, Model, Schema } from "@bunnykit/orm";
91
+
92
+ const connection = new Connection({ url: "sqlite://app.db" });
93
+ Model.setConnection(connection);
94
+ Schema.setConnection(connection);
95
+ ```
96
+
97
+ ### Create Tables
98
+
99
+ ```ts
100
+ import { Schema } from "@bunnykit/orm";
101
+
102
+ await Schema.create("users", (table) => {
103
+ table.increments("id");
104
+ table.string("name");
105
+ table.string("email").unique();
106
+ table.timestamps(); // created_at & updated_at
107
+ });
108
+
109
+ await Schema.create("posts", (table) => {
110
+ table.increments("id");
111
+ table.integer("user_id").unsigned();
112
+ table.string("title");
113
+ table.text("body").nullable();
114
+ table.timestamps();
115
+ });
116
+ ```
117
+
118
+ ### CRUD Operations
119
+
120
+ ```ts
121
+ // Create
122
+ const user = await User.create({ name: "Alice", email: "alice@example.com" });
123
+
124
+ // Find
125
+ const found = await User.find(1);
126
+
127
+ // Query
128
+ const adults = await User.where("age", ">=", 18)
129
+ .orderBy("name")
130
+ .get();
131
+
132
+ // Update
133
+ user.name = "Alice Smith";
134
+ await user.save();
135
+
136
+ // Delete
137
+ await user.delete();
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Schema Builder
143
+
144
+ ### Creating Tables
145
+
146
+ ```ts
147
+ await Schema.create("products", (table) => {
148
+ table.increments("id");
149
+ table.uuid("uuid").unique();
150
+ table.string("name", 100);
151
+ table.text("description").nullable();
152
+ table.integer("stock").unsigned().default(0);
153
+ table.decimal("price", 10, 2);
154
+ table.boolean("active").default(true);
155
+ table.json("metadata").nullable();
156
+ table.timestamps();
157
+ table.softDeletes(); // deleted_at
158
+ });
159
+ ```
160
+
161
+ ### Available Column Types
162
+
163
+ | Method | Description |
164
+ |--------|-------------|
165
+ | `increments(name)` | Auto-incrementing integer primary key |
166
+ | `bigIncrements(name)` | Auto-incrementing big integer |
167
+ | `string(name, length=255)` | VARCHAR |
168
+ | `text(name)` | TEXT |
169
+ | `integer(name)` | INTEGER |
170
+ | `bigInteger(name)` | BIGINT |
171
+ | `smallInteger(name)` | SMALLINT |
172
+ | `tinyInteger(name)` | TINYINT |
173
+ | `float(name, p=8, s=2)` | FLOAT |
174
+ | `double(name, p=8, s=2)` | DOUBLE |
175
+ | `decimal(name, p=8, s=2)` | DECIMAL |
176
+ | `boolean(name)` | BOOLEAN |
177
+ | `date(name)` | DATE |
178
+ | `dateTime(name)` | DATETIME |
179
+ | `time(name)` | TIME |
180
+ | `timestamp(name)` | TIMESTAMP |
181
+ | `json(name)` | JSON |
182
+ | `jsonb(name)` | JSONB (Postgres) |
183
+ | `binary(name)` | BLOB / BYTEA |
184
+ | `uuid(name)` | UUID |
185
+ | `enum(name, values)` | ENUM |
186
+
187
+ ### Column Modifiers
188
+
189
+ ```ts
190
+ table.string("email").unique(); // UNIQUE index
191
+ table.string("slug").index(); // INDEX
192
+ table.string("name").nullable(); // NULLABLE
193
+ table.integer("role").default(1); // DEFAULT value
194
+ table.string("code").comment("SKU code");
195
+ table.integer("user_id").unsigned();
196
+ ```
197
+
198
+ ### Altering Tables
199
+
200
+ ```ts
201
+ // Add columns
202
+ await Schema.table("users", (table) => {
203
+ table.string("phone").nullable();
204
+ table.timestamp("last_login").nullable();
205
+ });
206
+
207
+ // Rename
208
+ await Schema.rename("users", "customers");
209
+
210
+ // Drop
211
+ await Schema.drop("old_table");
212
+ await Schema.dropIfExists("old_table");
213
+ ```
214
+
215
+ ### Foreign Keys
216
+
217
+ ```ts
218
+ await Schema.create("posts", (table) => {
219
+ table.increments("id");
220
+ table.integer("user_id").unsigned();
221
+ table.foreign("user_id").references("id").on("users").onDelete("cascade");
222
+ });
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Query Builder
228
+
229
+ Every model exposes a query builder via static methods:
230
+
231
+ ```ts
232
+ // Static entry points
233
+ User.where("active", true);
234
+ User.where({ role: "admin", active: true });
235
+ User.whereIn("id", [1, 2, 3]);
236
+ User.whereNull("deleted_at");
237
+ User.whereNotNull("email");
238
+
239
+ // Chaining
240
+ const results = await User
241
+ .where("age", ">=", 18)
242
+ .whereIn("role", ["admin", "moderator"])
243
+ .orderBy("created_at", "desc")
244
+ .limit(10)
245
+ .offset(0)
246
+ .get();
247
+
248
+ // Aggregates
249
+ const count = await User.where("active", true).count();
250
+ const exists = await User.where("email", "test@example.com").exists();
251
+
252
+ // Joins
253
+ const posts = await Post
254
+ .query()
255
+ .select("posts.*", "users.name as author_name")
256
+ .join("users", "posts.user_id", "=", "users.id")
257
+ .get();
258
+
259
+ // Pluck
260
+ const emails = await User.pluck("email");
261
+
262
+ // First / Find
263
+ const user = await User.where("email", "alice@example.com").first();
264
+ const byId = await User.find(1);
265
+ ```
266
+
267
+ ---
268
+
269
+ ## Models
270
+
271
+ ### Conventions
272
+
273
+ - **Table name**: inferred from the class name in `snake_case` + plural `s`.
274
+ - `class User` → table `users`
275
+ - `class BlogPost` → table `blog_posts`
276
+ - **Primary key**: defaults to `id`
277
+ - **Timestamps**: `created_at` and `updated_at` are managed automatically (disable with `static timestamps = false`)
278
+
279
+ ### Defining Models
280
+
281
+ ```ts
282
+ class Product extends Model {
283
+ static table = "products"; // override table name
284
+ static primaryKey = "sku"; // override primary key
285
+ static timestamps = false; // disable timestamps
286
+ static softDeletes = true; // use deleted_at instead of hard deletes
287
+
288
+ static attributes = {
289
+ active: true,
290
+ status: "draft",
291
+ };
292
+
293
+ static casts = {
294
+ active: "boolean",
295
+ price: "decimal:2",
296
+ metadata: "json",
297
+ };
298
+ }
299
+ ```
300
+
301
+ ### Model Methods
302
+
303
+ ```ts
304
+ // Static
305
+ const all = await User.all();
306
+ const user = await User.create({ name: "Alice" });
307
+ const found = await User.find(1);
308
+ const first = await User.first();
309
+ const builder = User.where("active", true);
310
+
311
+ // Instance
312
+ user.fill({ name: "Bob", email: "bob@example.com" });
313
+ user.name; // property access
314
+ user.name = "Charlie"; // property assignment
315
+ user.getAttribute("name"); // explicit access still works
316
+ user.setAttribute("name", "Dana");
317
+ user.isDirty(); // true if attributes changed
318
+ user.getDirty(); // { name: "Charlie" }
319
+ await user.save();
320
+ await user.delete();
321
+ await user.refresh();
322
+ user.toJSON(); // plain object
323
+ ```
324
+
325
+ ### Default Attributes
326
+
327
+ Use `static attributes` to give new model instances in-memory defaults before saving:
328
+
329
+ ```ts
330
+ class User extends Model {
331
+ static attributes = {
332
+ active: true,
333
+ role: "member",
334
+ };
335
+ }
336
+
337
+ const user = new User({ name: "Ada" });
338
+ user.active; // true
339
+ user.role; // "member"
340
+ ```
341
+
342
+ These are model defaults, not database defaults. Values provided by the caller override them.
343
+
344
+ ### Attribute Casting
345
+
346
+ `static casts` transforms values on read and serializes them on write:
347
+
348
+ ```ts
349
+ class User extends Model {
350
+ static casts = {
351
+ active: "boolean",
352
+ login_count: "integer",
353
+ price: "decimal:2",
354
+ settings: "json",
355
+ secret: "encrypted",
356
+ };
357
+ }
358
+
359
+ const user = new User({
360
+ active: true,
361
+ settings: { theme: "dark" },
362
+ });
363
+
364
+ user.$attributes.active; // 1
365
+ user.active; // true
366
+ user.settings.theme; // "dark"
367
+ ```
368
+
369
+ Supported built-in casts:
370
+
371
+ | Cast | Behavior |
372
+ |------|----------|
373
+ | `boolean`, `bool` | Stores `1` / `0`, reads boolean |
374
+ | `number`, `integer`, `int`, `float`, `double` | Reads/writes numbers |
375
+ | `decimal:2` | Stores fixed precision string |
376
+ | `string` | Reads/writes string |
377
+ | `date`, `datetime` | Reads as `Date`, stores ISO string for `Date` input |
378
+ | `json`, `array`, `object` | Stores JSON string, reads parsed value |
379
+ | `enum` | Stores enum `.value` when present |
380
+ | `encrypted` | Base64 encodes on write and decodes on read |
381
+
382
+ Custom casts can implement `CastsAttributes`:
383
+
384
+ ```ts
385
+ import type { CastsAttributes, Model } from "@bunnykit/orm";
386
+
387
+ class UppercaseCast implements CastsAttributes {
388
+ get(_model: Model, _key: string, value: unknown) {
389
+ return String(value).toLowerCase();
390
+ }
391
+
392
+ set(_model: Model, _key: string, value: unknown) {
393
+ return String(value).toUpperCase();
394
+ }
395
+ }
396
+
397
+ class User extends Model {
398
+ static casts = {
399
+ code: UppercaseCast,
400
+ };
401
+ }
402
+ ```
403
+
404
+ You can also add instance-only casts at runtime:
405
+
406
+ ```ts
407
+ user.mergeCasts({ count: "string" });
408
+ ```
409
+
410
+ ### Soft Deletes
411
+
412
+ Enable soft deletes with `static softDeletes = true` and a `deleted_at` column:
413
+
414
+ ```ts
415
+ class User extends Model {
416
+ static softDeletes = true;
417
+ }
418
+
419
+ await user.delete(); // sets deleted_at
420
+ await user.restore(); // clears deleted_at
421
+ await user.forceDelete(); // permanently deletes
422
+
423
+ await User.all(); // excludes trashed rows
424
+ await User.withTrashed().get(); // includes trashed rows
425
+ await User.onlyTrashed().get(); // only trashed rows
426
+ await User.onlyTrashed().restore();
427
+ ```
428
+
429
+ ### Scopes
430
+
431
+ Local scopes are static methods named `scopeName`:
432
+
433
+ ```ts
434
+ class User extends Model {
435
+ static scopeActive(query) {
436
+ return query.where("active", true);
437
+ }
438
+ }
439
+
440
+ const users = await User.scope("active").get();
441
+ ```
442
+
443
+ Global scopes apply automatically to all queries:
444
+
445
+ ```ts
446
+ User.addGlobalScope("tenant", (query) => {
447
+ query.where("tenant_id", 1);
448
+ });
449
+
450
+ await User.withoutGlobalScope("tenant").get();
451
+ await User.withoutGlobalScopes().get();
452
+ ```
453
+
454
+ ---
455
+
456
+ ## Relationships
457
+
458
+ ### Standard Relations
459
+
460
+ ```ts
461
+ class User extends Model {
462
+ posts() {
463
+ return this.hasMany(Post); // foreignKey: user_id, localKey: id
464
+ }
465
+
466
+ profile() {
467
+ return this.hasOne(Profile); // foreignKey: user_id, localKey: id
468
+ }
469
+ }
470
+
471
+ class Post extends Model {
472
+ author() {
473
+ return this.belongsTo(User); // foreignKey: user_id, ownerKey: id
474
+ }
475
+ }
476
+ ```
477
+
478
+ Keys are **automatically inferred** from the model names. You can override them:
479
+
480
+ ```ts
481
+ this.hasMany(Post, "author_id", "uuid");
482
+ this.belongsTo(User, "author_id", "uuid");
483
+ ```
484
+
485
+ ### Belongs To Helpers
486
+
487
+ Use `associate` and `dissociate` to update the foreign key for a `belongsTo` relation:
488
+
489
+ ```ts
490
+ const post = new Post({ title: "Draft" });
491
+ post.author().associate(user);
492
+ post.user_id; // user's id
493
+
494
+ post.author().dissociate();
495
+ post.user_id; // null
496
+ ```
497
+
498
+ ### Through Relations
499
+
500
+ Use `hasManyThrough` and `hasOneThrough` for distant relations through an intermediate model:
501
+
502
+ ```ts
503
+ class Country extends Model {
504
+ posts() {
505
+ return this.hasManyThrough(Post, User);
506
+ }
507
+
508
+ profile() {
509
+ return this.hasOneThrough(Profile, User);
510
+ }
511
+ }
512
+ ```
513
+
514
+ By convention, Bunny expects:
515
+
516
+ - intermediate table foreign key to parent: `country_id`
517
+ - final table foreign key to intermediate: `user_id`
518
+
519
+ You can override keys:
520
+
521
+ ```ts
522
+ this.hasManyThrough(Post, User, "country_uuid", "author_id", "uuid", "id");
523
+ this.hasOneThrough(Profile, User, "country_id", "user_id");
524
+ ```
525
+
526
+ ### One-of-Many Relations
527
+
528
+ Convert a `hasMany` relation into a single latest, oldest, or aggregate-selected relation:
529
+
530
+ ```ts
531
+ class User extends Model {
532
+ posts() {
533
+ return this.hasMany(Post);
534
+ }
535
+
536
+ latestPost() {
537
+ return this.posts().latestOfMany("id");
538
+ }
539
+
540
+ oldestPost() {
541
+ return this.posts().oldestOfMany("id");
542
+ }
543
+
544
+ highestScoringPost() {
545
+ return this.posts().ofMany("score", "max");
546
+ }
547
+ }
548
+
549
+ const post = await user.latestPost().getResults();
550
+ ```
551
+
552
+ ### Relation Queries and Aggregates
553
+
554
+ Filter models by related records:
555
+
556
+ ```ts
557
+ const usersWithPosts = await User.has("posts").get();
558
+
559
+ const usersWithPublishedPosts = await User.whereHas("posts", (query) => {
560
+ query.where("status", "published");
561
+ }).get();
562
+
563
+ const usersWithoutPosts = await User.doesntHave("posts").get();
564
+ ```
565
+
566
+ Add relation aggregate columns:
567
+
568
+ ```ts
569
+ const users = await User
570
+ .withCount("posts")
571
+ .withSum("posts", "views")
572
+ .withAvg("posts", "score")
573
+ .withMin("posts", "created_at")
574
+ .withMax("posts", "created_at")
575
+ .get();
576
+
577
+ users[0].posts_count;
578
+ users[0].posts_sum_views;
579
+ ```
580
+
581
+ ### Polymorphic Relations
582
+
583
+ ```ts
584
+ import { Model, MorphMap } from "@bunnykit/orm";
585
+
586
+ // Register morph types so morphTo knows which model to instantiate
587
+ MorphMap.register("Post", Post);
588
+ MorphMap.register("Video", Video);
589
+
590
+ class Comment extends Model {
591
+ commentable() {
592
+ return this.morphTo("commentable"); // reads commentable_type / commentable_id
593
+ }
594
+ }
595
+
596
+ class Post extends Model {
597
+ comments() {
598
+ return this.morphMany(Comment, "commentable");
599
+ }
600
+ }
601
+
602
+ class Video extends Model {
603
+ comments() {
604
+ return this.morphMany(Comment, "commentable");
605
+ }
606
+
607
+ thumbnail() {
608
+ return this.morphOne(Image, "imageable");
609
+ }
610
+ }
611
+ ```
612
+
613
+ ### Many-to-Many Polymorphic
614
+
615
+ ```ts
616
+ class Post extends Model {
617
+ tags() {
618
+ return this.morphToMany(Tag, "taggable");
619
+ }
620
+ }
621
+
622
+ class Tag extends Model {
623
+ posts() {
624
+ return this.morphedByMany(Post, "taggable");
625
+ }
626
+ }
627
+ ```
628
+
629
+ Pivot table: `taggables(tag_id, taggable_id, taggable_type)`.
630
+
631
+ ### Customizing Morph Type
632
+
633
+ ```ts
634
+ class Post extends Model {
635
+ static morphName = "post"; // stored in {name}_type column
636
+ }
637
+ ```
638
+
639
+ ---
640
+
641
+ ## Observers
642
+
643
+ Register observers to hook into model lifecycle events:
644
+
645
+ ```ts
646
+ import { ObserverRegistry } from "@bunnykit/orm";
647
+
648
+ ObserverRegistry.register(User, {
649
+ async creating(model) {
650
+ console.log("About to create:", model.getAttribute("email"));
651
+ },
652
+ async created(model) {
653
+ console.log("Created user id:", model.getAttribute("id"));
654
+ },
655
+ async updating(model) {
656
+ console.log("User is changing:", model.getDirty());
657
+ },
658
+ async updated(model) {
659
+ // ...
660
+ },
661
+ async saving(model) {
662
+ // Runs before both create and update
663
+ },
664
+ async saved(model) {
665
+ // Runs after both create and update
666
+ },
667
+ async deleting(model) {
668
+ // ...
669
+ },
670
+ async deleted(model) {
671
+ // ...
672
+ },
673
+ });
674
+ ```
675
+
676
+ ---
677
+
678
+ ## Migrations
679
+
680
+ ### CLI Commands
681
+
682
+ ```bash
683
+ # Create a new migration file
684
+ bun run bunny migrate:make CreateUsersTable
685
+
686
+ # Run all pending migrations
687
+ bun run bunny migrate
688
+
689
+ # Rollback the last batch
690
+ bun run bunny migrate:rollback
691
+
692
+ # Show migration status
693
+ bun run bunny migrate:status
694
+ ```
695
+
696
+ ### Migration File Structure
697
+
698
+ ```ts
699
+ import { Migration, Schema } from "@bunnykit/orm";
700
+
701
+ export default class CreateUsersTable extends Migration {
702
+ async up(): Promise<void> {
703
+ await Schema.create("users", (table) => {
704
+ table.increments("id");
705
+ table.string("name");
706
+ table.string("email").unique();
707
+ table.timestamps();
708
+ });
709
+ }
710
+
711
+ async down(): Promise<void> {
712
+ await Schema.dropIfExists("users");
713
+ }
714
+ }
715
+ ```
716
+
717
+ Migrations are tracked in a `migrations` table (auto-created on first run).
718
+
719
+ ### Auto Type Generation
720
+
721
+ If you set `typesOutDir` in your config, types are **automatically regenerated** after every `migrate` and `migrate:rollback`:
722
+
723
+ ```bash
724
+ bun run bunny migrate
725
+ # → Migrated: 2026xxxx_create_users_table.ts
726
+ # → Regenerated types in ./src/generated/models
727
+ ```
728
+
729
+ No extra step needed — your models stay in sync with the schema automatically.
730
+
731
+ ---
732
+
733
+ ## Using as a Library (Programmatic Migrations)
734
+
735
+ ```ts
736
+ import { Connection, Migrator, MigrationCreator } from "@bunnykit/orm";
737
+
738
+ const connection = new Connection({ url: "sqlite://app.db" });
739
+
740
+ // Create a migration file
741
+ const creator = new MigrationCreator();
742
+ const path = await creator.create("CreateOrdersTable", "./database/migrations");
743
+
744
+ // Run migrations
745
+ const migrator = new Migrator(connection, "./database/migrations");
746
+ await migrator.run();
747
+
748
+ // Rollback
749
+ await migrator.rollback();
750
+ ```
751
+
752
+ ---
753
+
754
+ ## TypeScript Tips
755
+
756
+ ### Typing Model Attributes
757
+
758
+ ```ts
759
+ interface UserAttributes {
760
+ id: number;
761
+ name: string;
762
+ email: string;
763
+ created_at: string;
764
+ }
765
+
766
+ class User extends Model {
767
+ static table = "users";
768
+ }
769
+
770
+ // Type inference works automatically on static methods
771
+ const user = await User.create({ name: "Alice", email: "a@example.com" });
772
+ // user is typed as User
773
+ ```
774
+
775
+ ### Query Builder Typing
776
+
777
+ ```ts
778
+ // Builder<T> is inferred from the model class
779
+ const builder = User.where("name", "Alice"); // Builder<User>
780
+ const users: User[] = await builder.get();
781
+ ```
782
+
783
+ ---
784
+
785
+ ## Type Generation (IntelliSense)
786
+
787
+ Bunny can introspect your database schema and generate TypeScript declaration files for your existing models. This gives you **full IntelliSense** for model properties without changing your model source files:
788
+
789
+ ```ts
790
+ const user = await User.first();
791
+ user.name; // ✅ autocomplete + type-checking
792
+ user.email = "a@example.com"; // ✅ typed setter
793
+ ```
794
+
795
+ ### Generate Types
796
+
797
+ ```bash
798
+ # Generate into default directory (./generated/models)
799
+ bun run bunny types:generate
800
+
801
+ # Generate into a custom directory
802
+ bun run bunny types:generate ./src/generated
803
+ ```
804
+
805
+ Or configure in `bunny.config.ts`:
806
+
807
+ ```ts
808
+ export default {
809
+ connection: { url: "sqlite://app.db" },
810
+ migrationsPath: "./database/migrations",
811
+ typesOutDir: "./src/generated/model-types", // auto-regenerate .d.ts files on every migration
812
+ typeDeclarationImportPrefix: "../models",
813
+ // Optional overrides for non-conventional model names or paths:
814
+ typeDeclarations: {
815
+ admin_users: { path: "../models/AdminAccount", className: "AdminAccount" },
816
+ },
817
+ };
818
+ ```
819
+
820
+ With `typeDeclarationImportPrefix`, Bunny conventionally maps tables to singular PascalCase model modules:
821
+
822
+ | Table | Generated augmentation |
823
+ |-------|------------------------|
824
+ | `users` | `../models/User` / `User` |
825
+ | `blog_posts` | `../models/BlogPost` / `BlogPost` |
826
+ | `categories` | `../models/Category` / `Category` |
827
+
828
+ Set `typeDeclarationSingularModels: false` if your model classes use plural names.
829
+
830
+ ### Using Generated Declarations
831
+
832
+ For each table, Bunny generates an `Attributes` interface. If you configure `typeDeclarations`, it also augments your real model class:
833
+
834
+ ```ts
835
+ // generated/model-types/users.d.ts
836
+ export interface UsersAttributes {
837
+ id: number;
838
+ name: string;
839
+ email: string | null;
840
+ created_at: string;
841
+ }
842
+
843
+ declare module "../models/User" {
844
+ interface User extends UsersAttributes {}
845
+ }
846
+ ```
847
+
848
+ Your actual model stays hand-written:
849
+
850
+ ```ts
851
+ // models/User.ts
852
+ import { Model } from "@bunnykit/orm";
853
+
854
+ export class User extends Model {
855
+ static table = "users";
856
+
857
+ posts() {
858
+ return this.hasMany(Post);
859
+ }
860
+ }
861
+ ```
862
+
863
+ Editors that include the generated `.d.ts` files in `tsconfig.json` will understand `user.name`, `user.email`, etc. The generated files can be safely **gitignored** and regenerated whenever your schema changes.
864
+
865
+ If you still want generated base classes, use the programmatic generator with `{ stubs: true }`.
866
+
867
+ ### Manual Typing (Without Codegen)
868
+
869
+ If you prefer not to use codegen, you can pass a type parameter directly:
870
+
871
+ ```ts
872
+ interface UserAttributes {
873
+ id: number;
874
+ name: string;
875
+ email: string;
876
+ }
877
+
878
+ class User extends Model<UserAttributes> {
879
+ static table = "users";
880
+ }
881
+
882
+ // $attributes and getAttribute are now typed
883
+ const user = await User.first();
884
+ user.getAttribute("name"); // string
885
+ user.$attributes.email; // string
886
+ ```
887
+
888
+ ---
889
+
890
+ ## Testing
891
+
892
+ Bunny includes a full test suite built with `bun:test`.
893
+
894
+ ```bash
895
+ bun test
896
+ ```
897
+
898
+ 92 tests covering connection management, schema grammars, query builder, model CRUD, casts, scopes, soft deletes, relations, observers, migrations, and type generation.
899
+
900
+ ---
901
+
902
+ ## License
903
+
904
+ MIT