peta-orm 0.3.0 → 0.4.1

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
  ---
@@ -15,75 +17,49 @@ const page = await Post.query().with("author").paginate(1, 20)
15
17
  ## Quick Start
16
18
 
17
19
  ```bash
18
- bun add peta-orm arktype kysely
19
- bun add -d kysely-bun-sqlite
20
+ bun add peta-orm arktype kysely @libsql/kysely-libsql @libsql/client
20
21
  ```
21
22
 
22
23
  ```ts
23
- // db.ts
24
- import { Database } from "bun:sqlite"
25
- import { BunSqliteDialect } from "kysely-bun-sqlite"
26
- import type { ColumnShape } from "peta-orm"
27
- import { Peta, $t, ArkTypeSchemaConfig, Model, HasMany } from "peta-orm"
24
+ import { createClient } from "@libsql/client"
25
+ import { LibsqlDialect } from "@libsql/kysely-libsql"
26
+ import { createORM, defineModel, t } from "peta-orm"
28
27
 
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 }) })
28
+ const orm = createORM({
29
+ dialect: new LibsqlDialect({ url: "file:my-app.db" }),
30
+ })
57
31
 
58
- // Explicit registration (rest params, no array wrapper)
59
- peta.registerAll(User, Post)
32
+ const User = defineModel("users", {
33
+ columns: { id: t.integer().primaryKey(), name: t.string(255), email: t.text().unique() },
34
+ })
60
35
 
61
- // Or auto-discover from directory (Bun only):
62
- // await peta.discover("./src/**/*.model.ts")
36
+ orm.registerAll(User)
63
37
 
64
- export { peta, User, Post }
38
+ const user = await User.insert({ name: "Alice", email: "alice@test.com" })
65
39
  ```
66
40
 
41
+ > [!TIP]
42
+ > See the 32 [runnable examples](./examples) for every feature. Run them with `bun run examples/XX-*.ts`.
43
+
67
44
  ---
68
45
 
69
- ## Why Peta ORM?
46
+ ## Why peta-orm?
70
47
 
71
- | Feature | Raw Kysely | Peta ORM |
48
+ | Feature | Raw Kysely | peta-orm |
72
49
  |---------|-----------|----------|
73
50
  | **Validation** | Manual | Automatic from column definitions via ArkType |
74
51
  | **Models** | Row types only | Class instances with `$save()`, `$delete()`, `$reload()` |
75
- | **Relations** | Manual JOINs | Declarative `HasMany`, `BelongsTo`, `HasOne`, `ManyToMany` |
52
+ | **Relations** | Manual JOINs | Declarative `hasMany`, `belongsTo`, `hasOne`, `manyToMany` |
76
53
  | **Eager loading** | Manual batch | `.with("posts.author")` — one line, batched queries |
77
54
  | **Hooks** | — | `beforeCreate`, `afterUpdate`, `beforeDelete`, etc. |
78
55
  | **Soft deletes** | — | `withTrashed()`, `onlyTrashed()`, `$restore()`, `$forceDelete()` |
79
56
  | **Casting** | — | `$casts: { meta: "json", flags: "boolean" }` |
80
57
  | **Serialization** | — | `$hidden`, `$visible`, `$appends`, accessors |
81
58
  | **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` |
59
+ | **Error handling** | Raw driver codes | `DatabaseError` with `UNIQUE_CONSTRAINT` across dialects |
84
60
  | **Conditional queries** | Manual if/else | `.when(condition, qb => ...)`, `.unless(condition, qb => ...)` |
85
- | **Migrations** | — | Auto-generate from models, CLI, `MigrationRunner` |
86
61
  | **Global scopes** | — | `addGlobalScope("active", qb => ...)` |
62
+ | **Polymorphic relations** | — | `morphTo`, `morphMany`, `morphOne` |
87
63
 
88
64
  ---
89
65
 
@@ -91,91 +67,76 @@ export { peta, User, Post }
91
67
 
92
68
  ### Column Types & Validation
93
69
 
94
- ```ts
95
- import type { ColumnShape } from "peta-orm"
70
+ Column definitions double as validation schemas — no separate validation step needed.
96
71
 
97
- const t = $t({ schema: new ArkTypeSchemaConfig() })
72
+ ```ts
73
+ const t = columnTypes({ schema: createArkTypeSchemaConfig() })
98
74
 
99
- class User extends Model {
100
- static override columns = {
75
+ const User = defineModel("users", {
76
+ columns: {
101
77
  id: t.integer().primaryKey(),
102
- name: t.string(255).min(2), // min length
103
- email: t.text().email().unique(), // email format + unique constraint
78
+ name: t.string(255).min(2), // min length
79
+ email: t.text().email().unique(), // email format + unique
104
80
  age: t.integer().nullable().min(0).max(150).default(0),
105
81
  role: t.enum("admin", "user").default("user"),
106
82
  score: t.double().nullable(),
107
- ...t.timestamps(), // createdAt, updatedAt
108
- } satisfies ColumnShape
109
- }
83
+ ...t.timestamps(), // createdAt, updatedAt
84
+ },
85
+ })
110
86
 
111
- class Post extends Model {
112
- static override columns = {
87
+ const Post = defineModel("posts", {
88
+ columns: {
113
89
  id: t.integer().primaryKey(),
114
- userId: t.integer().references(() => User, ["id"]), // foreign key
90
+ userId: t.integer(),
115
91
  title: t.string(255),
116
92
  slug: t.string().unique(),
117
93
  published: t.boolean().default(false),
118
- } satisfies ColumnShape
119
- }
94
+ },
95
+ })
120
96
  ```
121
97
 
122
98
  ### Relations & Eager Loading
123
99
 
124
100
  ```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
- }
101
+ const User = defineModel("users", {
102
+ columns: { id: t.integer().primaryKey(), name: t.string(255) },
103
+ relations: {
104
+ posts: hasMany(() => Post, { foreignKey: "userId" }),
105
+ profile: hasOne(() => Profile, { foreignKey: "userId" }),
106
+ },
107
+ })
131
108
 
132
109
  // 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()
110
+ const users = await User.query().with("posts.author").execute()
138
111
 
139
112
  // Lazy load after fetch
140
113
  await user.$load("posts")
141
- await collection.load("posts.author")
142
114
 
143
115
  // Relation query
144
- const posts = await user.$relatedQuery("posts").where("published", true).execute()
116
+ const posts = await User.relations.posts.query(user).where("published", true).execute()
145
117
 
146
118
  // Existence filters
147
119
  const authors = await User.query().has("posts").execute()
148
120
  const active = await User.query().whereHas("posts", (q) => q.where("published", true)).execute()
149
121
  ```
150
122
 
151
- ### ManyToMany
123
+ ### Polymorphic Relations
152
124
 
153
125
  ```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
- }
126
+ const Comment = defineModel("comments", {
127
+ columns: { id: t.integer().primaryKey(), body: t.text() },
128
+ relations: {
129
+ subject: morphTo(() => ({
130
+ Post: { foreignKey: "postId" },
131
+ Article: { foreignKey: "articleId" },
132
+ })),
133
+ },
134
+ })
168
135
 
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
- }
136
+ const Post = defineModel("posts", {
137
+ columns: { id: t.integer().primaryKey(), title: t.string(255) },
138
+ relations: { comments: morphMany(() => Comment, { morphType: "post" }) },
139
+ })
179
140
  ```
180
141
 
181
142
  ### CRUD & Pagination
@@ -199,137 +160,133 @@ await User.delete(1)
199
160
 
200
161
  // Paginate
201
162
  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
163
+ // → { data: Post[], total: 30, perPage: 20, currentPage: 1, lastPage: 2, hasMorePages: true }
208
164
  ```
209
165
 
210
166
  ### Hooks & Timestamps
211
167
 
212
168
  ```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
- }
169
+ User.on("beforeCreate", (user) => { user.email = user.email.toLowerCase() })
170
+ User.on("afterCreate", (user) => { console.log("Created:", user.get("id")) })
219
171
 
220
- User.registerTimestamps() // auto-set createdAt/updatedAt
172
+ // Timestamps plugin sets createdAt/updatedAt automatically
173
+ const Timestamped = defineModel("ts", {
174
+ columns: { ...t.timestamps(), ...t.integer().primaryKey(), name: t.string(255) },
175
+ }).use(timestamps())
221
176
  ```
222
177
 
223
178
  ### Soft Deletes
224
179
 
225
180
  ```ts
226
- User.registerSoftDeletes()
181
+ const SoftModel = defineModel("items", {
182
+ columns: { id: t.integer().primaryKey(), name: t.string(255), ...t.timestamps() },
183
+ }).use(softDeletes())
227
184
 
228
- await user.$delete() // sets deletedAt timestamp
229
- await user.$restore() // clears deletedAt
230
- await user.$forceDelete() // actually deletes
185
+ await item.$delete() // sets deletedAt
186
+ await item.$restore() // clears deletedAt
187
+ await item.$forceDelete() // actually deletes
231
188
 
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
189
+ const active = await SoftModel.query().execute() // excludes deleted
190
+ const all = await SoftModel.query().withTrashed().execute() // includes deleted
191
+ const trashed = await SoftModel.query().onlyTrashed().execute() // only deleted
235
192
  ```
236
193
 
237
- ### Attribute Casting & Serialization
194
+ ### Graph Operations
238
195
 
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
196
+ Insert or upsert nested models in a single call:
257
197
 
258
198
  ```ts
259
- User.addGlobalScope("active", (qb) => qb.where("active", "=", 1))
260
-
261
- // Query without the scope
262
- await User.query().withoutGlobalScope("active").execute()
199
+ const user = await User.insertGraph({
200
+ name: "Alice",
201
+ posts: [{ title: "Post 1" }, { title: "Post 2" }],
202
+ })
263
203
 
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()
204
+ const updated = await User.upsertGraph({
205
+ id: user.get("id"),
206
+ name: "Alice Updated",
207
+ posts: [{ id: 1, title: "Post 1 Updated" }, { title: "New Post" }],
268
208
  })
269
209
  ```
270
210
 
271
- ### Conditional Chaining
211
+ ### Casting & Serialization
272
212
 
273
213
  ```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
- ```
214
+ const User = defineModel("users", {
215
+ columns: {
216
+ id: t.integer().primaryKey(), name: t.string(255),
217
+ meta: t.json(), flags: t.boolean(), password: t.string(255),
218
+ },
219
+ $casts: { meta: "json", flags: "boolean" },
220
+ $hidden: ["password"],
221
+ })
285
222
 
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.
223
+ const json = user.$toJSON() // password excluded, meta parsed from JSON
224
+ ```
287
225
 
288
226
  ### Error Handling
289
227
 
290
- Database constraint violations (unique, foreign key) are normalized into a `DatabaseError` across SQLite, PostgreSQL, and MySQL:
291
-
292
228
  ```ts
293
- import { DatabaseError } from "peta-orm"
294
-
295
229
  try {
296
- const post = await Post.insert({ slug: "my-post", title: "..." })
230
+ await Post.insert({ slug: "my-post" })
297
231
  } catch (e) {
298
232
  if (e instanceof DatabaseError && e.code === "UNIQUE_CONSTRAINT") {
299
- // slug already taken return 400
300
- return c.json({ error: "Slug already taken" }, 400)
233
+ return c.json({ error: "Slug taken" }, 400)
301
234
  }
302
235
  throw e
303
236
  }
304
237
  ```
305
238
 
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`.
239
+ | Code | Meaning | Driver errors |
240
+ |------|---------|--------------|
241
+ | `UNIQUE_CONSTRAINT` | Duplicate value | `SQLITE_CONSTRAINT_UNIQUE`, PG `23505`, MySQL `ER_DUP_ENTRY` |
242
+ | `FOREIGN_KEY_CONSTRAINT` | Missing referenced row | `SQLITE_CONSTRAINT_FOREIGNKEY`, PG `23503`, MySQL `ER_NO_REFERENCED_ROW_2` |
312
243
 
313
- ### Collection Utilities
244
+ ### Collections
314
245
 
315
246
  ```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
247
  const col = await User.query().orderBy("id", "asc").collect()
322
- col.toJSON() // all items serialized in one call
323
248
  col.pluck("name") // ["Alice", "Bob"]
324
249
  col.groupBy("role") // { admin: [...], user: [...] }
325
250
  col.load("posts") // eager load relations
326
- col.sum("score") // aggregate helpers
327
- col.avg("age")
328
- col.unique("role")
329
- col.sortBy("name")
251
+ col.sum("score")
330
252
  col.chunk(10) // split into batches
331
- col.first() // first element
332
- col.at(0) // same as [0] on plain arrays
253
+ ```
254
+
255
+ ### Global Scopes & Conditional Chaining
256
+
257
+ ```ts
258
+ User.addGlobalScope("active", (qb) => qb.where("active", "=", 1))
259
+ await User.query().withoutGlobalScope("active").execute()
260
+
261
+ const posts = await Post.query()
262
+ .when(sort?.length, (q) => q.orderBy(sort[0]!, "asc"))
263
+ .unless(sort?.length, (q) => q.orderBy("createdAt", "desc"))
264
+ .execute()
265
+ ```
266
+
267
+ ---
268
+
269
+ ## Migrations
270
+
271
+ Generate and run migrations from model definitions:
272
+
273
+ ```ts
274
+ import { createMigrationRunner, createMigrationGenerator } from "peta-orm/migrator"
275
+
276
+ const runner = createMigrationRunner(kysely)
277
+ const gen = createMigrationGenerator()
278
+
279
+ const code = gen.generateInitialMigration(models)
280
+ await runner.up(migrationFiles)
281
+ ```
282
+
283
+ Or via the CLI:
284
+
285
+ ```bash
286
+ bun run bin/peta migrate:init
287
+ bun run bin/peta migrate:generate
288
+ bun run bin/peta migrate:up
289
+ bun run bin/peta migrate:status
333
290
  ```
334
291
 
335
292
  ---
@@ -342,69 +299,65 @@ All self-contained (inline SQLite, run directly):
342
299
  bun run examples/01-basic-setup.ts
343
300
  bun run examples/04-relations.ts
344
301
  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
302
  ```
353
303
 
354
304
  | # | Example | Topic |
355
305
  |---|---------|-------|
356
- | 01 | [basic-setup](./examples/01-basic-setup.ts) | Peta init + SQLite setup |
306
+ | 01 | [basic-setup](./examples/01-basic-setup.ts) | ORM init + SQLite setup |
357
307
  | 02 | [model-definition](./examples/02-model-definition.ts) | Columns, types, modifiers, timestamps |
358
308
  | 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 |
309
+ | 04 | [relations](./examples/04-relations.ts) | hasMany, belongsTo, hasOne, eager loading |
310
+ | 05 | [query-builder](./examples/05-query-builder.ts) | where, orderBy, join, has, whereHas |
311
+ | 06 | [hooks-timestamps](./examples/06-hooks-timestamps.ts) | beforeCreate, afterCreate, timestamps |
362
312
  | 07 | [soft-deletes](./examples/07-soft-deletes.ts) | $delete, $restore, $forceDelete, withTrashed |
363
313
  | 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` |
314
+ | 09 | [hono-integration](./examples/09-hono-integration.ts) | Hono app + DatabaseError handling |
365
315
  | 10 | [elysia-integration](./examples/10-elysia-integration.ts) | Elysia app stub |
366
316
  | 11 | [many-to-many](./examples/11-many-to-many.ts) | ManyToMany via pivot table |
367
317
  | 12 | [transactions](./examples/12-transactions.ts) | Model.transaction(), rollback |
368
318
  | 13 | [casting](./examples/13-casting.ts) | $casts, $hidden, $appends, accessors |
369
319
  | 14 | [global-scopes](./examples/14-global-scopes.ts) | addGlobalScope(), withoutGlobalScope() |
370
- | 15 | [batch](./examples/15-batch.ts) | insertMany, insertMany() |
320
+ | 15 | [batch](./examples/15-batch.ts) | insertMany |
371
321
  | 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()` |
322
+ | 17 | [instance-methods](./examples/17-instance-methods.ts) | fill, dirty, reset, $reload, $load |
323
+ | 18 | [advanced-queries](./examples/18-advanced-queries.ts) | groupBy/having, aggregate helpers, chunk |
324
+ | 19 | [collections-deep](./examples/19-collections-deep.ts) | Full Collection + Paginator API |
325
+ | 20 | [advanced-relations](./examples/20-advanced-relations.ts) | HasManyThrough, polymorphic morphs |
326
+ | 21 | [migrations](./examples/21-migrations.ts) | MigrationRunner, MigrationGenerator |
327
+ | 22 | [related-query-builder](./examples/22-related-query-builder.ts) | `$related()` — scoped relation queries |
328
+ | 23 | [attach-detach-sync](./examples/23-attach-detach-sync.ts) | Many-to-many pivot management |
379
329
  | 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 |
330
+ | 25 | [static-hooks](./examples/25-static-hooks.ts) | `asFindQuery()` + `cancelQuery()` |
331
+ | 26 | [repository-pattern](./examples/26-repository-pattern.ts) | `createRepo()` — custom query methods |
332
+ | 27 | [plugins-and-helpers](./examples/27-plugins-and-helpers.ts) | `.use()` plugin system + makeHelper() |
333
+ | 28 | [nested-create-update](./examples/28-nested-create-update.ts) | Create/update with related data in one call |
334
+ | 29 | [allow-graph](./examples/29-allow-graph.ts) | `allowGraph()` — recursive eager load whitelist |
335
+ | 30 | [polymorphic-relations](./examples/30-polymorphic-relations.ts) | MorphMany/MorphOne/MorphTo |
386
336
  | 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 |
337
+ | 32 | [accessors-mutators](./examples/32-accessors-mutators.ts) | `Attribute.make({ get, set })` |
388
338
 
389
339
  ---
390
340
 
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`) |
341
+ ## Database Support
342
+
343
+ | Database | Dialect package | Status |
344
+ |----------|----------------|--------|
345
+ | SQLite | `@libsql/kysely-libsql` + `@libsql/client` | Tested |
346
+ | PostgreSQL | `pg` | Tested via Docker |
347
+ | MySQL | `mysql2` | Tested via Docker |
348
+
349
+ ```bash
350
+ docker compose up -d # PostgreSQL 16 + MySQL 8.0
351
+ cd packages/orm
352
+ bun test test/integration/
353
+ ```
354
+
355
+ Set `INTEGRATION_SKIP_PG=1` or `INTEGRATION_SKIP_MYSQL=1` to skip specific databases.
405
356
 
406
357
  ---
407
358
 
408
- ## License
359
+ ## Related packages
409
360
 
410
- MIT
361
+ - [peta-auth](../auth) — Encrypted cookie sessions, JWT, OAuth
362
+ - [peta-docs](../docs) — OpenAPI 3.1 spec generation + Scalar UI
363
+ - [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"