peta-orm 0.3.0 → 0.4.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
@@ -1,13 +1,15 @@
1
- # Peta ORM
1
+ # peta-orm
2
2
 
3
- **Typed ORM for Bun, built on [Kysely](https://github.com/kysely-org/kysely)** with [ArkType](https://arktype.io) validation.
3
+ [![npm version](https://img.shields.io/npm/v/peta-orm?style=flat-square)](https://www.npmjs.com/package/peta-orm)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org)
5
+ [![License](https://img.shields.io/badge/License-MIT-yellow?style=flat-square)](LICENSE)
4
6
 
5
- Column types, relations with eager loading, lifecycle hooks, timestamps, soft deletes, casting, serialization control, global scopes, polymorphic relations, and more — all fully typed end-to-end.
7
+ A feature-rich ORM for Bun, built on [Kysely](https://kysely.dev) with [ArkType](https://arktype.io) validation. ActiveRecord-style models, typed relations, lazy/eager loading, lifecycle hooks, soft deletes, timestamps, casting, serialization control, global scopes, polymorphic relations, pagination, collections, and more — all fully typed end-to-end.
6
8
 
7
9
  ```ts
8
10
  const user = await User.insert({ name: "Alice", email: "a@b.com" })
9
- const posts = await user.$relatedQuery("posts").where("published", true).execute()
10
- const page = await Post.query().with("author").paginate(1, 20)
11
+ const posts = await User.relations.posts.query(user).where("published", true).execute()
12
+ const page = await Post.query().with("author").orderBy("id", "asc").paginate(1, 20)
11
13
  ```
12
14
 
13
15
  ---
@@ -20,70 +22,45 @@ bun add -d kysely-bun-sqlite
20
22
  ```
21
23
 
22
24
  ```ts
23
- // db.ts
24
25
  import { Database } from "bun:sqlite"
25
26
  import { BunSqliteDialect } from "kysely-bun-sqlite"
26
- import type { ColumnShape } from "peta-orm"
27
- import { Peta, $t, ArkTypeSchemaConfig, Model, HasMany } from "peta-orm"
27
+ import { createORM, defineModel, t } from "peta-orm"
28
28
 
29
- const t = $t({ schema: new ArkTypeSchemaConfig() })
30
-
31
- class User extends Model {
32
- static override table = "users"
33
- static override columns = {
34
- id: t.integer().primaryKey(),
35
- name: t.string(255).min(2),
36
- email: t.text().email().unique(),
37
- } satisfies ColumnShape
38
- static override relations = {
39
- posts: new HasMany(() => Post),
40
- }
41
- }
42
-
43
- class Post extends Model {
44
- static override table = "posts"
45
- static override columns = {
46
- id: t.integer().primaryKey(),
47
- userId: t.integer().references(() => User, ["id"]),
48
- title: t.string(255),
49
- } satisfies ColumnShape
50
- }
51
-
52
- const database = new Database("my-app.db")
53
- database.run(`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL)`)
54
- database.run(`CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY AUTOINCREMENT, userId INTEGER NOT NULL, title TEXT NOT NULL)`)
55
-
56
- const peta = new Peta({ dialect: new BunSqliteDialect({ database }) })
29
+ const orm = createORM({
30
+ dialect: new BunSqliteDialect({ database: new Database("my-app.db") }),
31
+ })
57
32
 
58
- // Explicit registration (rest params, no array wrapper)
59
- peta.registerAll(User, Post)
33
+ const User = defineModel("users", {
34
+ columns: { id: t.integer().primaryKey(), name: t.string(255), email: t.text().unique() },
35
+ })
60
36
 
61
- // Or auto-discover from directory (Bun only):
62
- // await peta.discover("./src/**/*.model.ts")
37
+ orm.registerAll(User)
63
38
 
64
- export { peta, User, Post }
39
+ const user = await User.insert({ name: "Alice", email: "alice@test.com" })
65
40
  ```
66
41
 
42
+ > [!TIP]
43
+ > See the 32 [runnable examples](./examples) for every feature. Run them with `bun run examples/XX-*.ts`.
44
+
67
45
  ---
68
46
 
69
- ## Why Peta ORM?
47
+ ## Why peta-orm?
70
48
 
71
- | Feature | Raw Kysely | Peta ORM |
49
+ | Feature | Raw Kysely | peta-orm |
72
50
  |---------|-----------|----------|
73
51
  | **Validation** | Manual | Automatic from column definitions via ArkType |
74
52
  | **Models** | Row types only | Class instances with `$save()`, `$delete()`, `$reload()` |
75
- | **Relations** | Manual JOINs | Declarative `HasMany`, `BelongsTo`, `HasOne`, `ManyToMany` |
53
+ | **Relations** | Manual JOINs | Declarative `hasMany`, `belongsTo`, `hasOne`, `manyToMany` |
76
54
  | **Eager loading** | Manual batch | `.with("posts.author")` — one line, batched queries |
77
55
  | **Hooks** | — | `beforeCreate`, `afterUpdate`, `beforeDelete`, etc. |
78
56
  | **Soft deletes** | — | `withTrashed()`, `onlyTrashed()`, `$restore()`, `$forceDelete()` |
79
57
  | **Casting** | — | `$casts: { meta: "json", flags: "boolean" }` |
80
58
  | **Serialization** | — | `$hidden`, `$visible`, `$appends`, accessors |
81
59
  | **Pagination** | Manual offset/limit | `.paginate(1, 20)` — returns `{ data, total, perPage, ... }` |
82
- | **Transactions** | Manual | `Model.transaction(fn)` |
83
- | **Error handling** | Raw driver codes | `DatabaseError` with `UNIQUE_CONSTRAINT` / `FOREIGN_KEY_CONSTRAINT` |
60
+ | **Error handling** | Raw driver codes | `DatabaseError` with `UNIQUE_CONSTRAINT` across dialects |
84
61
  | **Conditional queries** | Manual if/else | `.when(condition, qb => ...)`, `.unless(condition, qb => ...)` |
85
- | **Migrations** | — | Auto-generate from models, CLI, `MigrationRunner` |
86
62
  | **Global scopes** | — | `addGlobalScope("active", qb => ...)` |
63
+ | **Polymorphic relations** | — | `morphTo`, `morphMany`, `morphOne` |
87
64
 
88
65
  ---
89
66
 
@@ -91,91 +68,76 @@ export { peta, User, Post }
91
68
 
92
69
  ### Column Types & Validation
93
70
 
94
- ```ts
95
- import type { ColumnShape } from "peta-orm"
71
+ Column definitions double as validation schemas — no separate validation step needed.
96
72
 
97
- const t = $t({ schema: new ArkTypeSchemaConfig() })
73
+ ```ts
74
+ const t = columnTypes({ schema: createArkTypeSchemaConfig() })
98
75
 
99
- class User extends Model {
100
- static override columns = {
76
+ const User = defineModel("users", {
77
+ columns: {
101
78
  id: t.integer().primaryKey(),
102
- name: t.string(255).min(2), // min length
103
- email: t.text().email().unique(), // email format + unique constraint
79
+ name: t.string(255).min(2), // min length
80
+ email: t.text().email().unique(), // email format + unique
104
81
  age: t.integer().nullable().min(0).max(150).default(0),
105
82
  role: t.enum("admin", "user").default("user"),
106
83
  score: t.double().nullable(),
107
- ...t.timestamps(), // createdAt, updatedAt
108
- } satisfies ColumnShape
109
- }
84
+ ...t.timestamps(), // createdAt, updatedAt
85
+ },
86
+ })
110
87
 
111
- class Post extends Model {
112
- static override columns = {
88
+ const Post = defineModel("posts", {
89
+ columns: {
113
90
  id: t.integer().primaryKey(),
114
- userId: t.integer().references(() => User, ["id"]), // foreign key
91
+ userId: t.integer(),
115
92
  title: t.string(255),
116
93
  slug: t.string().unique(),
117
94
  published: t.boolean().default(false),
118
- } satisfies ColumnShape
119
- }
95
+ },
96
+ })
120
97
  ```
121
98
 
122
99
  ### Relations & Eager Loading
123
100
 
124
101
  ```ts
125
- class User extends Model {
126
- static override relations = {
127
- posts: new HasMany(() => Post, { foreignKey: "userId" }),
128
- profile: new HasOne(() => Profile, { foreignKey: "userId" }),
129
- }
130
- }
102
+ const User = defineModel("users", {
103
+ columns: { id: t.integer().primaryKey(), name: t.string(255) },
104
+ relations: {
105
+ posts: hasMany(() => Post, { foreignKey: "userId" }),
106
+ profile: hasOne(() => Profile, { foreignKey: "userId" }),
107
+ },
108
+ })
131
109
 
132
110
  // Eager load with dot notation
133
- const users = await User.query()
134
- .with("posts")
135
- .with("posts.author")
136
- .with({ posts: (q) => q.where("published", true) })
137
- .execute()
111
+ const users = await User.query().with("posts.author").execute()
138
112
 
139
113
  // Lazy load after fetch
140
114
  await user.$load("posts")
141
- await collection.load("posts.author")
142
115
 
143
116
  // Relation query
144
- const posts = await user.$relatedQuery("posts").where("published", true).execute()
117
+ const posts = await User.relations.posts.query(user).where("published", true).execute()
145
118
 
146
119
  // Existence filters
147
120
  const authors = await User.query().has("posts").execute()
148
121
  const active = await User.query().whereHas("posts", (q) => q.where("published", true)).execute()
149
122
  ```
150
123
 
151
- ### ManyToMany
124
+ ### Polymorphic Relations
152
125
 
153
126
  ```ts
154
- class Post extends Model {
155
- static override columns = { id: t.integer().primaryKey(), title: t.string(255) } satisfies ColumnShape
156
- static override relations = {
157
- tags: new ManyToMany(() => Tag, {
158
- through: "post_tags",
159
- foreignPivotKey: "postId",
160
- relatedPivotKey: "tagId",
161
- }),
162
- }
163
- }
164
-
165
- class Tag extends Model {
166
- static override columns = { id: t.integer().primaryKey(), name: t.string(255) } satisfies ColumnShape
167
- }
127
+ const Comment = defineModel("comments", {
128
+ columns: { id: t.integer().primaryKey(), body: t.text() },
129
+ relations: {
130
+ subject: morphTo(() => ({
131
+ Post: { foreignKey: "postId" },
132
+ Article: { foreignKey: "articleId" },
133
+ })),
134
+ },
135
+ })
168
136
 
169
- // Pivot tables are regular Models — register them so the migration
170
- // generator includes the pivot table automatically.
171
- class PostTag extends Model {
172
- static override table = "post_tags"
173
- static override columns = {
174
- id: t.integer().primaryKey(),
175
- postId: t.integer().references(() => Post, ["id"]),
176
- tagId: t.integer().references(() => Tag, ["id"]),
177
- } satisfies ColumnShape
178
- }
137
+ const Post = defineModel("posts", {
138
+ columns: { id: t.integer().primaryKey(), title: t.string(255) },
139
+ relations: { comments: morphMany(() => Comment, { morphType: "post" }) },
140
+ })
179
141
  ```
180
142
 
181
143
  ### CRUD & Pagination
@@ -199,137 +161,133 @@ await User.delete(1)
199
161
 
200
162
  // Paginate
201
163
  const page = await Post.query().orderBy("id", "asc").paginate(1, 20)
202
- // → { data: Post[], total, perPage, currentPage, lastPage, hasMorePages }
203
-
204
- // Query results are plain T[] — standard, zero overhead
205
- const posts = await Post.query().where("published", true).execute()
206
- // posts: Post[]
207
- posts[0] // direct index access
164
+ // → { data: Post[], total: 30, perPage: 20, currentPage: 1, lastPage: 2, hasMorePages: true }
208
165
  ```
209
166
 
210
167
  ### Hooks & Timestamps
211
168
 
212
169
  ```ts
213
- class User extends Model {
214
- static {
215
- this.on("beforeCreate", (user) => { user.email = user.email.toLowerCase() })
216
- this.on("afterCreate", (user) => { console.log("Created:", user.get("id")) })
217
- }
218
- }
170
+ User.on("beforeCreate", (user) => { user.email = user.email.toLowerCase() })
171
+ User.on("afterCreate", (user) => { console.log("Created:", user.get("id")) })
219
172
 
220
- User.registerTimestamps() // auto-set createdAt/updatedAt
173
+ // Timestamps plugin sets createdAt/updatedAt automatically
174
+ const Timestamped = defineModel("ts", {
175
+ columns: { ...t.timestamps(), ...t.integer().primaryKey(), name: t.string(255) },
176
+ }).use(timestamps())
221
177
  ```
222
178
 
223
179
  ### Soft Deletes
224
180
 
225
181
  ```ts
226
- User.registerSoftDeletes()
182
+ const SoftModel = defineModel("items", {
183
+ columns: { id: t.integer().primaryKey(), name: t.string(255), ...t.timestamps() },
184
+ }).use(softDeletes())
227
185
 
228
- await user.$delete() // sets deletedAt timestamp
229
- await user.$restore() // clears deletedAt
230
- await user.$forceDelete() // actually deletes
186
+ await item.$delete() // sets deletedAt
187
+ await item.$restore() // clears deletedAt
188
+ await item.$forceDelete() // actually deletes
231
189
 
232
- const active = await User.query().execute() // excludes deleted
233
- const all = await User.query().withTrashed().execute() // includes deleted
234
- const trashed = await User.query().onlyTrashed().execute() // only deleted
190
+ const active = await SoftModel.query().execute() // excludes deleted
191
+ const all = await SoftModel.query().withTrashed().execute() // includes deleted
192
+ const trashed = await SoftModel.query().onlyTrashed().execute() // only deleted
235
193
  ```
236
194
 
237
- ### Attribute Casting & Serialization
195
+ ### Graph Operations
238
196
 
239
- ```ts
240
- class User extends Model {
241
- static override $casts = {
242
- meta: "json",
243
- flags: "boolean",
244
- createdAt: "date",
245
- }
246
- static override $hidden = ["password"]
247
- static override $visible = ["id", "name", "email"] // whitelist
248
- static override $appends = ["fullName"]
249
-
250
- getFullNameAttribute() { return `${this.get("first")} ${this.get("last")}` }
251
- }
252
-
253
- const json = user.$toJSON() // password excluded, fullName appended, meta parsed
254
- ```
255
-
256
- ### Global Scopes & Transactions
197
+ Insert or upsert nested models in a single call:
257
198
 
258
199
  ```ts
259
- User.addGlobalScope("active", (qb) => qb.where("active", "=", 1))
260
-
261
- // Query without the scope
262
- await User.query().withoutGlobalScope("active").execute()
200
+ const user = await User.insertGraph({
201
+ name: "Alice",
202
+ posts: [{ title: "Post 1" }, { title: "Post 2" }],
203
+ })
263
204
 
264
- // Transactions
265
- await User.transaction(async (trx) => {
266
- await trx.insertInto("users").values({ name: "A" }).execute()
267
- await trx.insertInto("posts").values({ userId: 1, title: "B" }).execute()
205
+ const updated = await User.upsertGraph({
206
+ id: user.get("id"),
207
+ name: "Alice Updated",
208
+ posts: [{ id: 1, title: "Post 1 Updated" }, { title: "New Post" }],
268
209
  })
269
210
  ```
270
211
 
271
- ### Conditional Chaining
212
+ ### Casting & Serialization
272
213
 
273
214
  ```ts
274
- const posts = await Post.query()
275
- .where("published", "=", published ?? 1)
276
- .when(sort?.length, (q) => {
277
- for (const s of sort) {
278
- q.orderBy(s.replace(/^-/, ""), s.startsWith("-") ? "desc" : "asc")
279
- }
280
- return q
281
- })
282
- .unless(sort?.length, (q) => q.orderBy("createdAt", "desc"))
283
- .execute()
284
- ```
215
+ const User = defineModel("users", {
216
+ columns: {
217
+ id: t.integer().primaryKey(), name: t.string(255),
218
+ meta: t.json(), flags: t.boolean(), password: t.string(255),
219
+ },
220
+ $casts: { meta: "json", flags: "boolean" },
221
+ $hidden: ["password"],
222
+ })
285
223
 
286
- Both `.when(condition, fn)` and `.unless(condition, fn)` return the query builder, keeping the chain intact. If the condition is truthy, `.when()` applies the callback; `.unless()` does the opposite.
224
+ const json = user.$toJSON() // password excluded, meta parsed from JSON
225
+ ```
287
226
 
288
227
  ### Error Handling
289
228
 
290
- Database constraint violations (unique, foreign key) are normalized into a `DatabaseError` across SQLite, PostgreSQL, and MySQL:
291
-
292
229
  ```ts
293
- import { DatabaseError } from "peta-orm"
294
-
295
230
  try {
296
- const post = await Post.insert({ slug: "my-post", title: "..." })
231
+ await Post.insert({ slug: "my-post" })
297
232
  } catch (e) {
298
233
  if (e instanceof DatabaseError && e.code === "UNIQUE_CONSTRAINT") {
299
- // slug already taken return 400
300
- return c.json({ error: "Slug already taken" }, 400)
234
+ return c.json({ error: "Slug taken" }, 400)
301
235
  }
302
236
  throw e
303
237
  }
304
238
  ```
305
239
 
306
- | `DatabaseError.code` | Meaning | Triggered by |
307
- |---|---|---|
308
- | `UNIQUE_CONSTRAINT` | Duplicate value on a unique column | `SQLITE_CONSTRAINT_UNIQUE`, PostgreSQL `23505`, MySQL `ER_DUP_ENTRY` |
309
- | `FOREIGN_KEY_CONSTRAINT` | Referenced row doesn't exist | `SQLITE_CONSTRAINT_FOREIGNKEY`, PostgreSQL `23503`, MySQL `ER_NO_REFERENCED_ROW_2` |
310
-
311
- The error also carries the `table` name and the original driver error via `cause`.
240
+ | Code | Meaning | Driver errors |
241
+ |------|---------|--------------|
242
+ | `UNIQUE_CONSTRAINT` | Duplicate value | `SQLITE_CONSTRAINT_UNIQUE`, PG `23505`, MySQL `ER_DUP_ENTRY` |
243
+ | `FOREIGN_KEY_CONSTRAINT` | Missing referenced row | `SQLITE_CONSTRAINT_FOREIGNKEY`, PG `23503`, MySQL `ER_NO_REFERENCED_ROW_2` |
312
244
 
313
- ### Collection Utilities
245
+ ### Collections
314
246
 
315
247
  ```ts
316
- // .execute() returns a plain array — lightweight, direct index access
317
- const users = await User.query().execute()
318
- users[0] // direct access
319
-
320
- // .collect() returns a Collection with convenience methods
321
248
  const col = await User.query().orderBy("id", "asc").collect()
322
- col.toJSON() // all items serialized in one call
323
249
  col.pluck("name") // ["Alice", "Bob"]
324
250
  col.groupBy("role") // { admin: [...], user: [...] }
325
251
  col.load("posts") // eager load relations
326
- col.sum("score") // aggregate helpers
327
- col.avg("age")
328
- col.unique("role")
329
- col.sortBy("name")
252
+ col.sum("score")
330
253
  col.chunk(10) // split into batches
331
- col.first() // first element
332
- col.at(0) // same as [0] on plain arrays
254
+ ```
255
+
256
+ ### Global Scopes & Conditional Chaining
257
+
258
+ ```ts
259
+ User.addGlobalScope("active", (qb) => qb.where("active", "=", 1))
260
+ await User.query().withoutGlobalScope("active").execute()
261
+
262
+ const posts = await Post.query()
263
+ .when(sort?.length, (q) => q.orderBy(sort[0]!, "asc"))
264
+ .unless(sort?.length, (q) => q.orderBy("createdAt", "desc"))
265
+ .execute()
266
+ ```
267
+
268
+ ---
269
+
270
+ ## Migrations
271
+
272
+ Generate and run migrations from model definitions:
273
+
274
+ ```ts
275
+ import { createMigrationRunner, createMigrationGenerator } from "peta-orm/migrator"
276
+
277
+ const runner = createMigrationRunner(kysely)
278
+ const gen = createMigrationGenerator()
279
+
280
+ const code = gen.generateInitialMigration(models)
281
+ await runner.up(migrationFiles)
282
+ ```
283
+
284
+ Or via the CLI:
285
+
286
+ ```bash
287
+ bun run bin/peta migrate:init
288
+ bun run bin/peta migrate:generate
289
+ bun run bin/peta migrate:up
290
+ bun run bin/peta migrate:status
333
291
  ```
334
292
 
335
293
  ---
@@ -342,69 +300,65 @@ All self-contained (inline SQLite, run directly):
342
300
  bun run examples/01-basic-setup.ts
343
301
  bun run examples/04-relations.ts
344
302
  bun run examples/07-soft-deletes.ts
345
-
346
- # CLI — manage migrations
347
- bun run bin/peta --help
348
- bun run bin/peta migrate:init
349
- bun run bin/peta migrate:generate CreateUsers
350
- bun run bin/peta migrate:up
351
- bun run bin/peta migrate:status
352
303
  ```
353
304
 
354
305
  | # | Example | Topic |
355
306
  |---|---------|-------|
356
- | 01 | [basic-setup](./examples/01-basic-setup.ts) | Peta init + SQLite setup |
307
+ | 01 | [basic-setup](./examples/01-basic-setup.ts) | ORM init + SQLite setup |
357
308
  | 02 | [model-definition](./examples/02-model-definition.ts) | Columns, types, modifiers, timestamps |
358
309
  | 03 | [crud](./examples/03-crud.ts) | insert, find, update, delete, paginate |
359
- | 04 | [relations](./examples/04-relations.ts) | HasMany, BelongsTo, HasOne, eager loading |
360
- | 05 | [query-builder](./examples/05-query-builder.ts) | where, orderBy, join, has, whereHas, whereDoesntHave, count |
361
- | 06 | [hooks-timestamps](./examples/06-hooks-timestamps.ts) | beforeCreate, afterCreate, registerTimestamps |
310
+ | 04 | [relations](./examples/04-relations.ts) | hasMany, belongsTo, hasOne, eager loading |
311
+ | 05 | [query-builder](./examples/05-query-builder.ts) | where, orderBy, join, has, whereHas |
312
+ | 06 | [hooks-timestamps](./examples/06-hooks-timestamps.ts) | beforeCreate, afterCreate, timestamps |
362
313
  | 07 | [soft-deletes](./examples/07-soft-deletes.ts) | $delete, $restore, $forceDelete, withTrashed |
363
314
  | 08 | [collection-paginator](./examples/08-collection-paginator.ts) | Collection, Paginator, `.collect()` |
364
- | 09 | [hono-integration](./examples/09-hono-integration.ts) | Hono app + error handling with `DatabaseError` |
315
+ | 09 | [hono-integration](./examples/09-hono-integration.ts) | Hono app + DatabaseError handling |
365
316
  | 10 | [elysia-integration](./examples/10-elysia-integration.ts) | Elysia app stub |
366
317
  | 11 | [many-to-many](./examples/11-many-to-many.ts) | ManyToMany via pivot table |
367
318
  | 12 | [transactions](./examples/12-transactions.ts) | Model.transaction(), rollback |
368
319
  | 13 | [casting](./examples/13-casting.ts) | $casts, $hidden, $appends, accessors |
369
320
  | 14 | [global-scopes](./examples/14-global-scopes.ts) | addGlobalScope(), withoutGlobalScope() |
370
- | 15 | [batch](./examples/15-batch.ts) | insertMany, insertMany() |
321
+ | 15 | [batch](./examples/15-batch.ts) | insertMany |
371
322
  | 16 | [discover](./examples/16-discover.ts) | peta.discover(), rest params |
372
- | 17 | [instance-methods](./examples/17-instance-methods.ts) | fill, dirty, reset, $reload, $load, $relatedQuery |
373
- | 18 | [advanced-queries](./examples/18-advanced-queries.ts) | groupBy/having, sum/avg/min/max, chunk, toSQL, updateMany |
374
- | 19 | [collections-deep](./examples/19-collections-deep.ts) | full Collection + Paginator API |
375
- | 20 | [advanced-relations](./examples/20-advanced-relations.ts) | HasManyThrough, polymorphic morphs, pivot extras |
376
- | 21 | [migrations](./examples/21-migrations.ts) | MigrationRunner, MigrationGenerator, CLI |
377
- | 22 | [related-query-builder](./examples/22-related-query-builder.ts) | `$related()` — scoped query builder for relations |
378
- | 23 | [attach-detach-sync](./examples/23-attach-detach-sync.ts) | Many-to-many pivot management via `$related()` |
323
+ | 17 | [instance-methods](./examples/17-instance-methods.ts) | fill, dirty, reset, $reload, $load |
324
+ | 18 | [advanced-queries](./examples/18-advanced-queries.ts) | groupBy/having, aggregate helpers, chunk |
325
+ | 19 | [collections-deep](./examples/19-collections-deep.ts) | Full Collection + Paginator API |
326
+ | 20 | [advanced-relations](./examples/20-advanced-relations.ts) | HasManyThrough, polymorphic morphs |
327
+ | 21 | [migrations](./examples/21-migrations.ts) | MigrationRunner, MigrationGenerator |
328
+ | 22 | [related-query-builder](./examples/22-related-query-builder.ts) | `$related()` — scoped relation queries |
329
+ | 23 | [attach-detach-sync](./examples/23-attach-detach-sync.ts) | Many-to-many pivot management |
379
330
  | 24 | [computed-columns](./examples/24-computed-columns.ts) | Runtime + batch async computed columns |
380
- | 25 | [static-hooks](./examples/25-static-hooks.ts) | `asFindQuery()` preview + `cancelQuery()` abort |
381
- | 26 | [repository-pattern](./examples/26-repository-pattern.ts) | `createRepo()` — chainable custom query methods |
382
- | 27 | [plugins-and-helpers](./examples/27-plugins-and-helpers.ts) | `.use()` plugin system + `makeHelper()` |
383
- | 28 | [nested-create-update](./examples/28-nested-create-update.ts) | Create and update models with related data in a single call |
384
- | 29 | [allow-graph](./examples/29-allow-graph.ts) | `allowGraph()` — recursive whitelist for eager loading |
385
- | 30 | [polymorphic-relations](./examples/30-polymorphic-relations.ts) | Polymorphic MorphMany/MorphOne/MorphTo with runtime resolution |
331
+ | 25 | [static-hooks](./examples/25-static-hooks.ts) | `asFindQuery()` + `cancelQuery()` |
332
+ | 26 | [repository-pattern](./examples/26-repository-pattern.ts) | `createRepo()` — custom query methods |
333
+ | 27 | [plugins-and-helpers](./examples/27-plugins-and-helpers.ts) | `.use()` plugin system + makeHelper() |
334
+ | 28 | [nested-create-update](./examples/28-nested-create-update.ts) | Create/update with related data in one call |
335
+ | 29 | [allow-graph](./examples/29-allow-graph.ts) | `allowGraph()` — recursive eager load whitelist |
336
+ | 30 | [polymorphic-relations](./examples/30-polymorphic-relations.ts) | MorphMany/MorphOne/MorphTo |
386
337
  | 31 | [graph-operations](./examples/31-graph-operations.ts) | `insertGraph()`/`upsertGraph()` with `#id`/`#ref` |
387
- | 32 | [accessors-mutators](./examples/32-accessors-mutators.ts) | `Attribute.make({ get, set })` — accessors and mutators |
338
+ | 32 | [accessors-mutators](./examples/32-accessors-mutators.ts) | `Attribute.make({ get, set })` |
388
339
 
389
340
  ---
390
341
 
391
- ## API Overview
392
-
393
- | Module | Key exports | File |
394
- |--------|-------------|------|
395
- | **Core** | `Peta`, `Model`, `$t`, `Collection` | `src/index.ts` |
396
- | **Discovery** | `peta.discover(glob)`, `peta.registerAll(...models)` | `src/peta.ts` |
397
- | **Columns** | `t.integer()`, `t.string()`, `t.email()`, `.min()`, `.max()`, `.nullable()`, `.default()` | `src/columns/column-types.ts` |
398
- | **Builders** | `.where()`, `.with()`, `.paginate()`, `.chunk()`, `.sum()`, `.toSQL()`, `.when()`, `.unless()`, `.collect()` | `src/builder/query-builder.ts` |
399
- | **Relations** | `HasMany`, `BelongsTo`, `HasOne`, `ManyToMany`, `HasManyThrough` | `src/relations/Relation.ts` |
400
- | **Polymorphic** | `MorphTo`, `MorphMany`, `MorphOne` | `src/relations/Morph.ts` |
401
- | **Hooks** | `HookManager`, `on()`, `off()`, `trigger()` | `src/hooks/lifecycle.ts` |
402
- | **Paginator** | `Paginator`, `.paginate()` | `src/pagination/Paginator.ts` |
403
- | **Errors** | `ModelNotFoundError`, `RelationNotFoundError`, `ValidationError`, `DatabaseError` | `src/errors/errors.ts` |
404
- | **Migrations** | `MigrationRunner`, `MigrationGenerator`, `defineConfig`, CLI (`peta migrate:*`) | `src/migrations/index.ts` (import from `peta-orm/migrator`) |
342
+ ## Database Support
343
+
344
+ | Database | Dialect package | Status |
345
+ |----------|----------------|--------|
346
+ | SQLite | `kysely-bun-sqlite` | Tested |
347
+ | PostgreSQL | `pg` | Tested via Docker |
348
+ | MySQL | `mysql2` | Tested via Docker |
349
+
350
+ ```bash
351
+ docker compose up -d # PostgreSQL 16 + MySQL 8.0
352
+ cd packages/orm
353
+ bun test test/integration/
354
+ ```
355
+
356
+ Set `INTEGRATION_SKIP_PG=1` or `INTEGRATION_SKIP_MYSQL=1` to skip specific databases.
405
357
 
406
358
  ---
407
359
 
408
- ## License
360
+ ## Related packages
409
361
 
410
- MIT
362
+ - [peta-auth](../auth) — Encrypted cookie sessions, JWT, OAuth
363
+ - [peta-docs](../docs) — OpenAPI 3.1 spec generation + Scalar UI
364
+ - [peta-migrate](../migrate) — Standalone migration runner and generator
@@ -153,7 +153,7 @@ function createCollection(items) {
153
153
  async load(...relations) {
154
154
  if (data.length === 0) return collection;
155
155
  const { EagerLoader } = await import("./index.mjs").then((n) => n.i);
156
- const { getModelDefFromInstance } = await import("./factory-rIbPGjRg.mjs").then((n) => n.n);
156
+ const { getModelDefFromInstance } = await import("./factory-BBvIMQuc.mjs").then((n) => n.n);
157
157
  const { getModelDef } = await import("./index.mjs").then((n) => n.n);
158
158
  const first = data[0];
159
159
  const def = getModelDefFromInstance(first) ?? getModelDef(first);
@@ -57,7 +57,7 @@ function getRuntime() {
57
57
  }
58
58
  //#endregion
59
59
  //#region src/model/types.ts
60
- const FORBIDDEN_KEYS = new Set([
60
+ const FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
61
61
  "__proto__",
62
62
  "constructor",
63
63
  "prototype"
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { n as createCollection } from "./collection-Dv3sQPMx.mjs";
2
+ import { n as createCollection } from "./collection-PFmrQHyM.mjs";
3
3
  import { a as ModelNotRegisteredError, c as ValidationError, i as ModelNotFoundError, n as normalizeError, o as RelationNotAllowedError, r as DatabaseError, s as RelationNotFoundError } from "./errors-sfFJolfu.mjs";
4
4
  import { a as registerSoftDeletesFor, n as getSoftDeleteConfig, o as registerTimestampsFor, r as hasSoftDelete, s as createHookManager, t as getHooksFor } from "./hooks-BD0xy7uw.mjs";
5
- import { a as castValue, r as initRuntime, t as createInstance } from "./factory-rIbPGjRg.mjs";
5
+ import { a as castValue, r as initRuntime, t as createInstance } from "./factory-BBvIMQuc.mjs";
6
6
  import { a as getRawRelations, d as setExists, o as getState } from "./state-LtlHp6XV.mjs";
7
- import { a as setConfig$1, n as reloadModel, r as saveModel, t as getConfig$1 } from "./save-B8rudcT5.mjs";
7
+ import { a as setConfig$1, n as reloadModel, r as saveModel, t as getConfig$1 } from "./save-D5UKXvqC.mjs";
8
8
  import { type } from "arktype";
9
9
  import { Kysely, sql } from "kysely";
10
10
  import { ulid as ulid$1 } from "ulid";
@@ -948,7 +948,7 @@ function createQueryBuilder(def, peta) {
948
948
  execute: runExecute,
949
949
  async collect() {
950
950
  const items = await runExecute();
951
- const { createCollection } = await import("./collection-Dv3sQPMx.mjs").then((n) => n.t);
951
+ const { createCollection } = await import("./collection-PFmrQHyM.mjs").then((n) => n.t);
952
952
  return createCollection(items);
953
953
  },
954
954
  async executeTakeFirst() {
@@ -1396,16 +1396,16 @@ function defineModel(table, config) {
1396
1396
  return this.query().first();
1397
1397
  },
1398
1398
  async create(data) {
1399
- return (await import("./save-B8rudcT5.mjs").then((n) => n.i)).insertModel(def, data);
1399
+ return (await import("./save-D5UKXvqC.mjs").then((n) => n.i)).insertModel(def, data);
1400
1400
  },
1401
1401
  async insert(data) {
1402
- return (await import("./save-B8rudcT5.mjs").then((n) => n.i)).insertModel(def, data);
1402
+ return (await import("./save-D5UKXvqC.mjs").then((n) => n.i)).insertModel(def, data);
1403
1403
  },
1404
1404
  async insertMany(dataArray) {
1405
- return (await import("./save-B8rudcT5.mjs").then((n) => n.i)).insertManyModel(def, dataArray);
1405
+ return (await import("./save-D5UKXvqC.mjs").then((n) => n.i)).insertManyModel(def, dataArray);
1406
1406
  },
1407
1407
  async update(id, data) {
1408
- return (await import("./save-B8rudcT5.mjs").then((n) => n.i)).updateModel(def, id, data);
1408
+ return (await import("./save-D5UKXvqC.mjs").then((n) => n.i)).updateModel(def, id, data);
1409
1409
  },
1410
1410
  async insertGraph(data, options) {
1411
1411
  return (await Promise.resolve().then(() => graph_exports)).insertGraph(def, data, options);
@@ -2157,7 +2157,10 @@ function hasMany(relatedThunk, options = {}) {
2157
2157
  function hasOne(relatedThunk, options = {}) {
2158
2158
  const base = hasMany(relatedThunk, options);
2159
2159
  const result = {};
2160
- for (const key of Object.keys(base)) Object.defineProperty(result, key, Object.getOwnPropertyDescriptor(base, key));
2160
+ for (const key of Object.keys(base)) {
2161
+ const desc = Object.getOwnPropertyDescriptor(base, key);
2162
+ Object.defineProperty(result, key, desc);
2163
+ }
2161
2164
  result.match = function match(models, results, relationName) {
2162
2165
  const grouped = groupByArray$1(results, base.foreignKey);
2163
2166
  for (const model of models) {
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  import { n as normalizeError, r as DatabaseError } from "./errors-sfFJolfu.mjs";
3
3
  import { t as getHooksFor } from "./hooks-BD0xy7uw.mjs";
4
- import { i as applyCastsToData, o as prepareForDb, t as createInstance } from "./factory-rIbPGjRg.mjs";
4
+ import { i as applyCastsToData, o as prepareForDb, t as createInstance } from "./factory-BBvIMQuc.mjs";
5
5
  import { d as setExists, f as syncOriginal, i as getExists, o as getState } from "./state-LtlHp6XV.mjs";
6
6
  //#region src/model/save.ts
7
7
  var save_exports = /* @__PURE__ */ __exportAll({
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "peta-orm",
3
3
  "type": "module",
4
4
  "private": false,
5
- "version": "0.3.0",
5
+ "version": "0.4.0",
6
6
  "description": "ORM for Bun, built on Kysely",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -38,7 +38,7 @@
38
38
  "ulid": "^3.0.2"
39
39
  },
40
40
  "peerDependencies": {
41
- "kysely": "^0.28.17",
41
+ "kysely": "^0.29.2",
42
42
  "typescript": "^6.0.0"
43
43
  },
44
44
  "devDependencies": {
@@ -46,8 +46,9 @@
46
46
  "@types/bun": "^1.3.14",
47
47
  "elysia": "^1.4.28",
48
48
  "hono": "^4.12.25",
49
- "kysely": "^0.28.17",
50
- "kysely-bun-sqlite": "^0.4.0",
49
+ "kysely": "^0.29.2",
50
+ "@libsql/kysely-libsql": "^0.4.1",
51
+ "@libsql/client": "^0.8.0",
51
52
  "mysql2": "^3.22.5",
52
53
  "pg": "^8.21.0",
53
54
  "tsdown": "^0.22.2",