peta-orm 0.2.6 → 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.
Files changed (97) hide show
  1. package/README.md +186 -210
  2. package/bin/peta +1 -1
  3. package/dist/collection-PFmrQHyM.mjs +173 -0
  4. package/dist/crud-BCWvg5MI.mjs +101 -0
  5. package/dist/errors-sfFJolfu.mjs +69 -0
  6. package/dist/factory-BBvIMQuc.mjs +173 -0
  7. package/dist/hooks-BD0xy7uw.mjs +77 -0
  8. package/dist/index-BdJnSMYi.d.mts +480 -0
  9. package/dist/index.d.mts +215 -0
  10. package/dist/index.mjs +2604 -0
  11. package/dist/migrations/cli.d.mts +4 -0
  12. package/dist/migrations/cli.mjs +74 -0
  13. package/dist/migrations/index.d.mts +53 -0
  14. package/dist/migrations/index.mjs +2 -0
  15. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  16. package/dist/runner-DQ7uT6LC.mjs +180 -0
  17. package/dist/save-D5UKXvqC.mjs +331 -0
  18. package/dist/state-LtlHp6XV.mjs +56 -0
  19. package/package.json +27 -16
  20. package/dist/builder/delete-builder.d.ts +0 -9
  21. package/dist/builder/delete-builder.d.ts.map +0 -1
  22. package/dist/builder/eager-loader.d.ts +0 -13
  23. package/dist/builder/eager-loader.d.ts.map +0 -1
  24. package/dist/builder/index.d.ts +0 -6
  25. package/dist/builder/index.d.ts.map +0 -1
  26. package/dist/builder/query-builder.d.ts +0 -57
  27. package/dist/builder/query-builder.d.ts.map +0 -1
  28. package/dist/builder/update-builder.d.ts +0 -9
  29. package/dist/builder/update-builder.d.ts.map +0 -1
  30. package/dist/collection/collection.d.ts +0 -48
  31. package/dist/collection/collection.d.ts.map +0 -1
  32. package/dist/collection-wwtv7qmv.js +0 -8
  33. package/dist/columns/arktype-config.d.ts +0 -8
  34. package/dist/columns/arktype-config.d.ts.map +0 -1
  35. package/dist/columns/column-types.d.ts +0 -27
  36. package/dist/columns/column-types.d.ts.map +0 -1
  37. package/dist/columns/column.d.ts +0 -30
  38. package/dist/columns/column.d.ts.map +0 -1
  39. package/dist/columns/schema-config.d.ts +0 -10
  40. package/dist/columns/schema-config.d.ts.map +0 -1
  41. package/dist/errors/errors.d.ts +0 -26
  42. package/dist/errors/errors.d.ts.map +0 -1
  43. package/dist/hooks/lifecycle.d.ts +0 -11
  44. package/dist/hooks/lifecycle.d.ts.map +0 -1
  45. package/dist/index-4xnrys72.js +0 -56
  46. package/dist/index-ddxdqxz6.js +0 -636
  47. package/dist/index-k18nf2r7.js +0 -18
  48. package/dist/index-qb301480.js +0 -2424
  49. package/dist/index-sm1xx8gs.js +0 -7828
  50. package/dist/index.d.ts +0 -24
  51. package/dist/index.d.ts.map +0 -1
  52. package/dist/index.js +0 -9229
  53. package/dist/integrations/elysia.d.ts +0 -12
  54. package/dist/integrations/elysia.d.ts.map +0 -1
  55. package/dist/integrations/hono.d.ts +0 -7
  56. package/dist/integrations/hono.d.ts.map +0 -1
  57. package/dist/migrations/cli.d.ts +0 -2
  58. package/dist/migrations/cli.d.ts.map +0 -1
  59. package/dist/migrations/cli.js +0 -4079
  60. package/dist/migrations/config.d.ts +0 -5
  61. package/dist/migrations/config.d.ts.map +0 -1
  62. package/dist/migrations/generator.d.ts +0 -9
  63. package/dist/migrations/generator.d.ts.map +0 -1
  64. package/dist/migrations/index.d.ts +0 -5
  65. package/dist/migrations/index.d.ts.map +0 -1
  66. package/dist/migrations/index.js +0 -17
  67. package/dist/migrations/runner.d.ts +0 -12
  68. package/dist/migrations/runner.d.ts.map +0 -1
  69. package/dist/migrations/types.d.ts +0 -26
  70. package/dist/migrations/types.d.ts.map +0 -1
  71. package/dist/model/model-delete.d.ts +0 -7
  72. package/dist/model/model-delete.d.ts.map +0 -1
  73. package/dist/model/model-hooks.d.ts +0 -10
  74. package/dist/model/model-hooks.d.ts.map +0 -1
  75. package/dist/model/model-relation.d.ts +0 -7
  76. package/dist/model/model-relation.d.ts.map +0 -1
  77. package/dist/model/model-save.d.ts +0 -6
  78. package/dist/model/model-save.d.ts.map +0 -1
  79. package/dist/model/model-scope.d.ts +0 -6
  80. package/dist/model/model-scope.d.ts.map +0 -1
  81. package/dist/model/model-serialize.d.ts +0 -3
  82. package/dist/model/model-serialize.d.ts.map +0 -1
  83. package/dist/model/model-state.d.ts +0 -27
  84. package/dist/model/model-state.d.ts.map +0 -1
  85. package/dist/model/model.d.ts +0 -88
  86. package/dist/model/model.d.ts.map +0 -1
  87. package/dist/pagination/paginator.d.ts +0 -30
  88. package/dist/pagination/paginator.d.ts.map +0 -1
  89. package/dist/paginator-tmp4hxj5.js +0 -9
  90. package/dist/peta.d.ts +0 -20
  91. package/dist/peta.d.ts.map +0 -1
  92. package/dist/relations/morph.d.ts +0 -44
  93. package/dist/relations/morph.d.ts.map +0 -1
  94. package/dist/relations/relation.d.ts +0 -85
  95. package/dist/relations/relation.d.ts.map +0 -1
  96. package/dist/types.d.ts +0 -18
  97. package/dist/types.d.ts.map +0 -1
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,128 +161,135 @@ 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 }
164
+ // → { data: Post[], total: 30, perPage: 20, currentPage: 1, lastPage: 2, hasMorePages: true }
203
165
  ```
204
166
 
205
167
  ### Hooks & Timestamps
206
168
 
207
169
  ```ts
208
- class User extends Model {
209
- static {
210
- this.on("beforeCreate", (user) => { user.email = user.email.toLowerCase() })
211
- this.on("afterCreate", (user) => { console.log("Created:", user.get("id")) })
212
- }
213
- }
170
+ User.on("beforeCreate", (user) => { user.email = user.email.toLowerCase() })
171
+ User.on("afterCreate", (user) => { console.log("Created:", user.get("id")) })
214
172
 
215
- 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())
216
177
  ```
217
178
 
218
179
  ### Soft Deletes
219
180
 
220
181
  ```ts
221
- User.registerSoftDeletes()
182
+ const SoftModel = defineModel("items", {
183
+ columns: { id: t.integer().primaryKey(), name: t.string(255), ...t.timestamps() },
184
+ }).use(softDeletes())
222
185
 
223
- await user.$delete() // sets deletedAt timestamp
224
- await user.$restore() // clears deletedAt
225
- await user.$forceDelete() // actually deletes
186
+ await item.$delete() // sets deletedAt
187
+ await item.$restore() // clears deletedAt
188
+ await item.$forceDelete() // actually deletes
226
189
 
227
- const active = await User.query().execute() // excludes deleted
228
- const all = await User.query().withTrashed().execute() // includes deleted
229
- 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
230
193
  ```
231
194
 
232
- ### Attribute Casting & Serialization
233
-
234
- ```ts
235
- class User extends Model {
236
- static override $casts = {
237
- meta: "json",
238
- flags: "boolean",
239
- createdAt: "date",
240
- }
241
- static override $hidden = ["password"]
242
- static override $visible = ["id", "name", "email"] // whitelist
243
- static override $appends = ["fullName"]
244
-
245
- getFullNameAttribute() { return `${this.get("first")} ${this.get("last")}` }
246
- }
247
-
248
- const json = user.$toJSON() // password excluded, fullName appended, meta parsed
249
- ```
195
+ ### Graph Operations
250
196
 
251
- ### Global Scopes & Transactions
197
+ Insert or upsert nested models in a single call:
252
198
 
253
199
  ```ts
254
- User.addGlobalScope("active", (qb) => qb.where("active", "=", 1))
255
-
256
- // Query without the scope
257
- await User.query().withoutGlobalScope("active").execute()
200
+ const user = await User.insertGraph({
201
+ name: "Alice",
202
+ posts: [{ title: "Post 1" }, { title: "Post 2" }],
203
+ })
258
204
 
259
- // Transactions
260
- await User.transaction(async (trx) => {
261
- await trx.insertInto("users").values({ name: "A" }).execute()
262
- 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" }],
263
209
  })
264
210
  ```
265
211
 
266
- ### Conditional Chaining
212
+ ### Casting & Serialization
267
213
 
268
214
  ```ts
269
- const posts = await Post.query()
270
- .where("published", "=", published ?? 1)
271
- .when(sort?.length, (q) => {
272
- for (const s of sort) {
273
- q.orderBy(s.replace(/^-/, ""), s.startsWith("-") ? "desc" : "asc")
274
- }
275
- return q
276
- })
277
- .unless(sort?.length, (q) => q.orderBy("createdAt", "desc"))
278
- .execute()
279
- ```
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
+ })
280
223
 
281
- 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
+ ```
282
226
 
283
227
  ### Error Handling
284
228
 
285
- Database constraint violations (unique, foreign key) are normalized into a `DatabaseError` across SQLite, PostgreSQL, and MySQL:
286
-
287
229
  ```ts
288
- import { DatabaseError } from "peta-orm"
289
-
290
230
  try {
291
- const post = await Post.insert({ slug: "my-post", title: "..." })
231
+ await Post.insert({ slug: "my-post" })
292
232
  } catch (e) {
293
233
  if (e instanceof DatabaseError && e.code === "UNIQUE_CONSTRAINT") {
294
- // slug already taken return 400
295
- return c.json({ error: "Slug already taken" }, 400)
234
+ return c.json({ error: "Slug taken" }, 400)
296
235
  }
297
236
  throw e
298
237
  }
299
238
  ```
300
239
 
301
- | `DatabaseError.code` | Meaning | Triggered by |
302
- |---|---|---|
303
- | `UNIQUE_CONSTRAINT` | Duplicate value on a unique column | `SQLITE_CONSTRAINT_UNIQUE`, PostgreSQL `23505`, MySQL `ER_DUP_ENTRY` |
304
- | `FOREIGN_KEY_CONSTRAINT` | Referenced row doesn't exist | `SQLITE_CONSTRAINT_FOREIGNKEY`, PostgreSQL `23503`, MySQL `ER_NO_REFERENCED_ROW_2` |
305
-
306
- 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` |
307
244
 
308
- ### Collection Utilities
245
+ ### Collections
309
246
 
310
247
  ```ts
311
- const users = await User.query().execute()
312
- const col = new Collection(users)
313
-
248
+ const col = await User.query().orderBy("id", "asc").collect()
314
249
  col.pluck("name") // ["Alice", "Bob"]
315
250
  col.groupBy("role") // { admin: [...], user: [...] }
316
251
  col.load("posts") // eager load relations
317
- col.sum("score") // aggregate helpers
318
- col.avg("age")
319
- col.unique("role")
320
- col.sortBy("name")
252
+ col.sum("score")
321
253
  col.chunk(10) // split into batches
322
254
  ```
323
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
291
+ ```
292
+
324
293
  ---
325
294
 
326
295
  ## Examples
@@ -331,58 +300,65 @@ All self-contained (inline SQLite, run directly):
331
300
  bun run examples/01-basic-setup.ts
332
301
  bun run examples/04-relations.ts
333
302
  bun run examples/07-soft-deletes.ts
334
-
335
- # CLI — manage migrations
336
- bun run bin/peta --help
337
- bun run bin/peta migrate:init
338
- bun run bin/peta migrate:generate CreateUsers
339
- bun run bin/peta migrate:up
340
- bun run bin/peta migrate:status
341
303
  ```
342
304
 
343
305
  | # | Example | Topic |
344
306
  |---|---------|-------|
345
- | 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 |
346
308
  | 02 | [model-definition](./examples/02-model-definition.ts) | Columns, types, modifiers, timestamps |
347
309
  | 03 | [crud](./examples/03-crud.ts) | insert, find, update, delete, paginate |
348
- | 04 | [relations](./examples/04-relations.ts) | HasMany, BelongsTo, HasOne, eager loading |
349
- | 05 | [query-builder](./examples/05-query-builder.ts) | where, orderBy, join, has, whereHas, whereDoesntHave, count |
350
- | 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 |
351
313
  | 07 | [soft-deletes](./examples/07-soft-deletes.ts) | $delete, $restore, $forceDelete, withTrashed |
352
314
  | 08 | [collection-paginator](./examples/08-collection-paginator.ts) | Collection, Paginator, `.collect()` |
353
- | 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 |
354
316
  | 10 | [elysia-integration](./examples/10-elysia-integration.ts) | Elysia app stub |
355
317
  | 11 | [many-to-many](./examples/11-many-to-many.ts) | ManyToMany via pivot table |
356
318
  | 12 | [transactions](./examples/12-transactions.ts) | Model.transaction(), rollback |
357
319
  | 13 | [casting](./examples/13-casting.ts) | $casts, $hidden, $appends, accessors |
358
320
  | 14 | [global-scopes](./examples/14-global-scopes.ts) | addGlobalScope(), withoutGlobalScope() |
359
- | 15 | [batch](./examples/15-batch.ts) | insertMany, insertMany() |
321
+ | 15 | [batch](./examples/15-batch.ts) | insertMany |
360
322
  | 16 | [discover](./examples/16-discover.ts) | peta.discover(), rest params |
361
- | 17 | [instance-methods](./examples/17-instance-methods.ts) | fill, dirty, reset, $reload, $load, $relatedQuery |
362
- | 18 | [advanced-queries](./examples/18-advanced-queries.ts) | groupBy/having, sum/avg/min/max, chunk, toSQL, updateMany |
363
- | 19 | [collections-deep](./examples/19-collections-deep.ts) | full Collection + Paginator API |
364
- | 20 | [advanced-relations](./examples/20-advanced-relations.ts) | HasManyThrough, polymorphic morphs, pivot extras |
365
- | 21 | [migrations](./examples/21-migrations.ts) | MigrationRunner, MigrationGenerator, CLI |
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 |
330
+ | 24 | [computed-columns](./examples/24-computed-columns.ts) | Runtime + batch async computed columns |
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 |
337
+ | 31 | [graph-operations](./examples/31-graph-operations.ts) | `insertGraph()`/`upsertGraph()` with `#id`/`#ref` |
338
+ | 32 | [accessors-mutators](./examples/32-accessors-mutators.ts) | `Attribute.make({ get, set })` |
366
339
 
367
340
  ---
368
341
 
369
- ## API Overview
370
-
371
- | Module | Key exports | File |
372
- |--------|-------------|------|
373
- | **Core** | `Peta`, `Model`, `$t`, `Collection` | `src/index.ts` |
374
- | **Discovery** | `peta.discover(glob)`, `peta.registerAll(...models)` | `src/peta.ts` |
375
- | **Columns** | `t.integer()`, `t.string()`, `t.email()`, `.min()`, `.max()`, `.nullable()`, `.default()` | `src/columns/column-types.ts` |
376
- | **Builders** | `.where()`, `.with()`, `.paginate()`, `.chunk()`, `.sum()`, `.toSQL()`, `.when()`, `.unless()`, `.collect()` | `src/builder/query-builder.ts` |
377
- | **Relations** | `HasMany`, `BelongsTo`, `HasOne`, `ManyToMany`, `HasManyThrough` | `src/relations/Relation.ts` |
378
- | **Polymorphic** | `MorphTo`, `MorphMany`, `MorphOne` | `src/relations/Morph.ts` |
379
- | **Hooks** | `HookManager`, `on()`, `off()`, `trigger()` | `src/hooks/lifecycle.ts` |
380
- | **Paginator** | `Paginator`, `.paginate()` | `src/pagination/Paginator.ts` |
381
- | **Errors** | `ModelNotFoundError`, `RelationNotFoundError`, `ValidationError`, `DatabaseError` | `src/errors/errors.ts` |
382
- | **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.
383
357
 
384
358
  ---
385
359
 
386
- ## License
360
+ ## Related packages
387
361
 
388
- 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
package/bin/peta CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bun
2
- import { run } from "../dist/migrations/cli.js"
2
+ import { run } from "../dist/migrations/cli.mjs"
3
3
  await run()