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.
- package/README.md +186 -210
- package/bin/peta +1 -1
- package/dist/collection-PFmrQHyM.mjs +173 -0
- package/dist/crud-BCWvg5MI.mjs +101 -0
- package/dist/errors-sfFJolfu.mjs +69 -0
- package/dist/factory-BBvIMQuc.mjs +173 -0
- package/dist/hooks-BD0xy7uw.mjs +77 -0
- package/dist/index-BdJnSMYi.d.mts +480 -0
- package/dist/index.d.mts +215 -0
- package/dist/index.mjs +2604 -0
- package/dist/migrations/cli.d.mts +4 -0
- package/dist/migrations/cli.mjs +74 -0
- package/dist/migrations/index.d.mts +53 -0
- package/dist/migrations/index.mjs +2 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/runner-DQ7uT6LC.mjs +180 -0
- package/dist/save-D5UKXvqC.mjs +331 -0
- package/dist/state-LtlHp6XV.mjs +56 -0
- package/package.json +27 -16
- package/dist/builder/delete-builder.d.ts +0 -9
- package/dist/builder/delete-builder.d.ts.map +0 -1
- package/dist/builder/eager-loader.d.ts +0 -13
- package/dist/builder/eager-loader.d.ts.map +0 -1
- package/dist/builder/index.d.ts +0 -6
- package/dist/builder/index.d.ts.map +0 -1
- package/dist/builder/query-builder.d.ts +0 -57
- package/dist/builder/query-builder.d.ts.map +0 -1
- package/dist/builder/update-builder.d.ts +0 -9
- package/dist/builder/update-builder.d.ts.map +0 -1
- package/dist/collection/collection.d.ts +0 -48
- package/dist/collection/collection.d.ts.map +0 -1
- package/dist/collection-wwtv7qmv.js +0 -8
- package/dist/columns/arktype-config.d.ts +0 -8
- package/dist/columns/arktype-config.d.ts.map +0 -1
- package/dist/columns/column-types.d.ts +0 -27
- package/dist/columns/column-types.d.ts.map +0 -1
- package/dist/columns/column.d.ts +0 -30
- package/dist/columns/column.d.ts.map +0 -1
- package/dist/columns/schema-config.d.ts +0 -10
- package/dist/columns/schema-config.d.ts.map +0 -1
- package/dist/errors/errors.d.ts +0 -26
- package/dist/errors/errors.d.ts.map +0 -1
- package/dist/hooks/lifecycle.d.ts +0 -11
- package/dist/hooks/lifecycle.d.ts.map +0 -1
- package/dist/index-4xnrys72.js +0 -56
- package/dist/index-ddxdqxz6.js +0 -636
- package/dist/index-k18nf2r7.js +0 -18
- package/dist/index-qb301480.js +0 -2424
- package/dist/index-sm1xx8gs.js +0 -7828
- package/dist/index.d.ts +0 -24
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -9229
- package/dist/integrations/elysia.d.ts +0 -12
- package/dist/integrations/elysia.d.ts.map +0 -1
- package/dist/integrations/hono.d.ts +0 -7
- package/dist/integrations/hono.d.ts.map +0 -1
- package/dist/migrations/cli.d.ts +0 -2
- package/dist/migrations/cli.d.ts.map +0 -1
- package/dist/migrations/cli.js +0 -4079
- package/dist/migrations/config.d.ts +0 -5
- package/dist/migrations/config.d.ts.map +0 -1
- package/dist/migrations/generator.d.ts +0 -9
- package/dist/migrations/generator.d.ts.map +0 -1
- package/dist/migrations/index.d.ts +0 -5
- package/dist/migrations/index.d.ts.map +0 -1
- package/dist/migrations/index.js +0 -17
- package/dist/migrations/runner.d.ts +0 -12
- package/dist/migrations/runner.d.ts.map +0 -1
- package/dist/migrations/types.d.ts +0 -26
- package/dist/migrations/types.d.ts.map +0 -1
- package/dist/model/model-delete.d.ts +0 -7
- package/dist/model/model-delete.d.ts.map +0 -1
- package/dist/model/model-hooks.d.ts +0 -10
- package/dist/model/model-hooks.d.ts.map +0 -1
- package/dist/model/model-relation.d.ts +0 -7
- package/dist/model/model-relation.d.ts.map +0 -1
- package/dist/model/model-save.d.ts +0 -6
- package/dist/model/model-save.d.ts.map +0 -1
- package/dist/model/model-scope.d.ts +0 -6
- package/dist/model/model-scope.d.ts.map +0 -1
- package/dist/model/model-serialize.d.ts +0 -3
- package/dist/model/model-serialize.d.ts.map +0 -1
- package/dist/model/model-state.d.ts +0 -27
- package/dist/model/model-state.d.ts.map +0 -1
- package/dist/model/model.d.ts +0 -88
- package/dist/model/model.d.ts.map +0 -1
- package/dist/pagination/paginator.d.ts +0 -30
- package/dist/pagination/paginator.d.ts.map +0 -1
- package/dist/paginator-tmp4hxj5.js +0 -9
- package/dist/peta.d.ts +0 -20
- package/dist/peta.d.ts.map +0 -1
- package/dist/relations/morph.d.ts +0 -44
- package/dist/relations/morph.d.ts.map +0 -1
- package/dist/relations/relation.d.ts +0 -85
- package/dist/relations/relation.d.ts.map +0 -1
- package/dist/types.d.ts +0 -18
- package/dist/types.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# peta-orm
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/peta-orm)
|
|
4
|
+
[](https://www.typescriptlang.org)
|
|
5
|
+
[](LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
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
|
|
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
|
|
27
|
-
import { Peta, $t, ArkTypeSchemaConfig, Model, HasMany } from "peta-orm"
|
|
27
|
+
import { createORM, defineModel, t } from "peta-orm"
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
59
|
-
|
|
33
|
+
const User = defineModel("users", {
|
|
34
|
+
columns: { id: t.integer().primaryKey(), name: t.string(255), email: t.text().unique() },
|
|
35
|
+
})
|
|
60
36
|
|
|
61
|
-
|
|
62
|
-
// await peta.discover("./src/**/*.model.ts")
|
|
37
|
+
orm.registerAll(User)
|
|
63
38
|
|
|
64
|
-
|
|
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
|
|
47
|
+
## Why peta-orm?
|
|
70
48
|
|
|
71
|
-
| Feature | Raw Kysely |
|
|
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 `
|
|
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
|
-
| **
|
|
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
|
-
|
|
95
|
-
import type { ColumnShape } from "peta-orm"
|
|
71
|
+
Column definitions double as validation schemas — no separate validation step needed.
|
|
96
72
|
|
|
97
|
-
|
|
73
|
+
```ts
|
|
74
|
+
const t = columnTypes({ schema: createArkTypeSchemaConfig() })
|
|
98
75
|
|
|
99
|
-
|
|
100
|
-
|
|
76
|
+
const User = defineModel("users", {
|
|
77
|
+
columns: {
|
|
101
78
|
id: t.integer().primaryKey(),
|
|
102
|
-
name: t.string(255).min(2),
|
|
103
|
-
email: t.text().email().unique(),
|
|
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(),
|
|
108
|
-
}
|
|
109
|
-
}
|
|
84
|
+
...t.timestamps(), // createdAt, updatedAt
|
|
85
|
+
},
|
|
86
|
+
})
|
|
110
87
|
|
|
111
|
-
|
|
112
|
-
|
|
88
|
+
const Post = defineModel("posts", {
|
|
89
|
+
columns: {
|
|
113
90
|
id: t.integer().primaryKey(),
|
|
114
|
-
userId: t.integer()
|
|
91
|
+
userId: t.integer(),
|
|
115
92
|
title: t.string(255),
|
|
116
93
|
slug: t.string().unique(),
|
|
117
94
|
published: t.boolean().default(false),
|
|
118
|
-
}
|
|
119
|
-
}
|
|
95
|
+
},
|
|
96
|
+
})
|
|
120
97
|
```
|
|
121
98
|
|
|
122
99
|
### Relations & Eager Loading
|
|
123
100
|
|
|
124
101
|
```ts
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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
|
-
###
|
|
124
|
+
### Polymorphic Relations
|
|
152
125
|
|
|
153
126
|
```ts
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
+
const SoftModel = defineModel("items", {
|
|
183
|
+
columns: { id: t.integer().primaryKey(), name: t.string(255), ...t.timestamps() },
|
|
184
|
+
}).use(softDeletes())
|
|
222
185
|
|
|
223
|
-
await
|
|
224
|
-
await
|
|
225
|
-
await
|
|
186
|
+
await item.$delete() // sets deletedAt
|
|
187
|
+
await item.$restore() // clears deletedAt
|
|
188
|
+
await item.$forceDelete() // actually deletes
|
|
226
189
|
|
|
227
|
-
const active = await
|
|
228
|
-
const all = await
|
|
229
|
-
const trashed = await
|
|
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
|
-
###
|
|
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
|
-
|
|
197
|
+
Insert or upsert nested models in a single call:
|
|
252
198
|
|
|
253
199
|
```ts
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
200
|
+
const user = await User.insertGraph({
|
|
201
|
+
name: "Alice",
|
|
202
|
+
posts: [{ title: "Post 1" }, { title: "Post 2" }],
|
|
203
|
+
})
|
|
258
204
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
###
|
|
212
|
+
### Casting & Serialization
|
|
267
213
|
|
|
268
214
|
```ts
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
+
await Post.insert({ slug: "my-post" })
|
|
292
232
|
} catch (e) {
|
|
293
233
|
if (e instanceof DatabaseError && e.code === "UNIQUE_CONSTRAINT") {
|
|
294
|
-
|
|
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
|
-
|
|
|
302
|
-
|
|
303
|
-
| `UNIQUE_CONSTRAINT` | Duplicate value
|
|
304
|
-
| `FOREIGN_KEY_CONSTRAINT` |
|
|
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
|
-
###
|
|
245
|
+
### Collections
|
|
309
246
|
|
|
310
247
|
```ts
|
|
311
|
-
const
|
|
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")
|
|
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) |
|
|
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) |
|
|
349
|
-
| 05 | [query-builder](./examples/05-query-builder.ts) | where, orderBy, join, has, whereHas
|
|
350
|
-
| 06 | [hooks-timestamps](./examples/06-hooks-timestamps.ts) | beforeCreate, afterCreate,
|
|
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 +
|
|
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
|
|
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
|
|
362
|
-
| 18 | [advanced-queries](./examples/18-advanced-queries.ts) | groupBy/having,
|
|
363
|
-
| 19 | [collections-deep](./examples/19-collections-deep.ts) |
|
|
364
|
-
| 20 | [advanced-relations](./examples/20-advanced-relations.ts) | HasManyThrough, polymorphic morphs
|
|
365
|
-
| 21 | [migrations](./examples/21-migrations.ts) | MigrationRunner, MigrationGenerator
|
|
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
|
-
##
|
|
370
|
-
|
|
371
|
-
|
|
|
372
|
-
|
|
373
|
-
|
|
|
374
|
-
|
|
|
375
|
-
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
##
|
|
360
|
+
## Related packages
|
|
387
361
|
|
|
388
|
-
|
|
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