vibeorm 1.1.2 → 1.1.4
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 +1096 -42
- package/package.json +6 -6
- package/src/commands/init.ts +303 -0
- package/src/index.ts +4 -0
package/README.md
CHANGED
|
@@ -1,71 +1,316 @@
|
|
|
1
|
-
#
|
|
1
|
+
# VibeORM
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A type-safe TypeScript ORM for **Bun + PostgreSQL**. Parses Prisma `.prisma` schema files and generates a fully typed client that executes raw SQL — no build step, no runtime overhead.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
|
|
9
|
+
- [Bun](https://bun.sh) >= 1.1.0
|
|
10
|
+
- PostgreSQL
|
|
11
|
+
|
|
12
|
+
### 1. Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun add vibeorm @vibeorm/runtime @vibeorm/adapter-bun
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or with `node-postgres`:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bun add vibeorm @vibeorm/runtime @vibeorm/adapter-pg pg
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 2. Initialize
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bunx vibeorm init
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This scaffolds your project with a starter schema, seed script, `.env` file, and adds convenience scripts to `package.json`.
|
|
31
|
+
|
|
32
|
+
### 3. Define your schema
|
|
33
|
+
|
|
34
|
+
Edit `prisma/schema.prisma`:
|
|
35
|
+
|
|
36
|
+
```prisma
|
|
37
|
+
datasource db {
|
|
38
|
+
provider = "postgresql"
|
|
39
|
+
url = env("DATABASE_URL")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
model User {
|
|
43
|
+
id Int @id @default(autoincrement())
|
|
44
|
+
createdAt DateTime @default(now())
|
|
45
|
+
updatedAt DateTime @updatedAt
|
|
46
|
+
email String @unique
|
|
47
|
+
name String?
|
|
48
|
+
posts Post[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
model Post {
|
|
52
|
+
id Int @id @default(autoincrement())
|
|
53
|
+
createdAt DateTime @default(now())
|
|
54
|
+
updatedAt DateTime @updatedAt
|
|
55
|
+
title String
|
|
56
|
+
content String?
|
|
57
|
+
published Boolean @default(false)
|
|
58
|
+
authorId Int
|
|
59
|
+
author User @relation(fields: [authorId], references: [id])
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 4. Generate and push
|
|
6
64
|
|
|
7
65
|
```bash
|
|
8
|
-
|
|
66
|
+
bunx vibeorm generate
|
|
67
|
+
bunx vibeorm db push --force
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 5. Use the client
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { VibeClient } from "./generated/vibeorm/index.ts";
|
|
74
|
+
import { bunAdapter } from "@vibeorm/adapter-bun";
|
|
75
|
+
|
|
76
|
+
const db = VibeClient({ adapter: bunAdapter() });
|
|
77
|
+
|
|
78
|
+
// Create
|
|
79
|
+
const user = await db.user.create({
|
|
80
|
+
data: { email: "alice@example.com", name: "Alice" },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Query with relations
|
|
84
|
+
const users = await db.user.findMany({
|
|
85
|
+
where: { email: { contains: "alice", mode: "insensitive" } },
|
|
86
|
+
include: { posts: true },
|
|
87
|
+
orderBy: { createdAt: "desc" },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Type-safe select — return type is narrowed to only { id, email }
|
|
91
|
+
const emails = await db.user.findMany({
|
|
92
|
+
select: { id: true, email: true },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await db.$disconnect();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Schema
|
|
101
|
+
|
|
102
|
+
VibeORM uses the Prisma `.prisma` DSL. All standard Prisma features are supported:
|
|
103
|
+
|
|
104
|
+
- **Scalar types:** `String`, `Int`, `BigInt`, `Float`, `Decimal`, `Boolean`, `DateTime`, `Json`, `Bytes`
|
|
105
|
+
- **Scalar lists:** `String[]`, `Int[]`, etc.
|
|
106
|
+
- **Enums** with `@@map`
|
|
107
|
+
- **ID fields:** `@id`, `@@id` (composite primary keys)
|
|
108
|
+
- **Unique constraints:** `@unique`, `@@unique` (compound)
|
|
109
|
+
- **Defaults:** `@default(autoincrement())`, `@default(now())`, `@default(uuid())`, `@default(cuid())`, `@default(nanoid())`, `@default(ulid())`
|
|
110
|
+
- **Auto-updated timestamps:** `@updatedAt`
|
|
111
|
+
- **Column/table mapping:** `@map`, `@@map`
|
|
112
|
+
- **Relations:** `@relation` for 1:1, 1:N, N:M (implicit join tables), self-referential
|
|
113
|
+
- **Indexes:** `@@index`
|
|
114
|
+
- **Multi-file schemas:** directory of `.prisma` files (loaded alphabetically)
|
|
115
|
+
- **Native type annotations:** `@db.VarChar(255)`, etc.
|
|
116
|
+
- **Custom ID prefixes:** `/// @vibeorm.idPrefix("usr_")`
|
|
117
|
+
|
|
118
|
+
### Multi-file schemas
|
|
119
|
+
|
|
120
|
+
Instead of a single schema file, you can split your schema across multiple files in a directory:
|
|
121
|
+
|
|
9
122
|
```
|
|
123
|
+
prisma/schema/
|
|
124
|
+
01-base.prisma # datasource block
|
|
125
|
+
02-user.prisma # User model
|
|
126
|
+
03-content.prisma # Post, Comment models
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Configure the `schema` option to point to the directory instead of a file.
|
|
130
|
+
|
|
131
|
+
---
|
|
10
132
|
|
|
11
|
-
## Commands
|
|
133
|
+
## CLI Commands
|
|
12
134
|
|
|
13
135
|
```bash
|
|
14
136
|
bunx vibeorm <command> [options]
|
|
15
137
|
```
|
|
16
138
|
|
|
17
|
-
###
|
|
139
|
+
### `init`
|
|
140
|
+
|
|
141
|
+
Scaffold a new VibeORM project in the current directory.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
bunx vibeorm init
|
|
145
|
+
bunx vibeorm init --empty # No example models
|
|
146
|
+
bunx vibeorm init --adapter pg # Use node-postgres in templates
|
|
147
|
+
bunx vibeorm init --schema ./db/schema.prisma --output ./src/generated
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Creates:
|
|
151
|
+
- `prisma/schema.prisma` — starter schema with `User` and `Post` models
|
|
152
|
+
- `prisma/seed.ts` — seed script using the generated client
|
|
153
|
+
- `.env` — `DATABASE_URL` placeholder
|
|
154
|
+
- Updates `package.json` with `vibeorm` config and convenience scripts
|
|
155
|
+
|
|
156
|
+
| Flag | Description |
|
|
157
|
+
|------|-------------|
|
|
158
|
+
| `--empty` | Skip example models, create only the datasource block |
|
|
159
|
+
| `--adapter <name>` | Adapter for seed template: `bun` (default) or `pg` |
|
|
160
|
+
| `--schema <path>` | Custom schema path (default: `./prisma/schema.prisma`) |
|
|
161
|
+
| `--output <path>` | Custom output directory (default: `./generated/vibeorm`) |
|
|
162
|
+
|
|
163
|
+
### `generate`
|
|
164
|
+
|
|
165
|
+
Parse the `.prisma` schema and generate TypeScript client files.
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
bunx vibeorm generate
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Generates 8 TypeScript files in the configured output directory:
|
|
172
|
+
|
|
173
|
+
| File | Contents |
|
|
174
|
+
|------|----------|
|
|
175
|
+
| `index.ts` | `VibeClient()` factory, model metadata, re-exports |
|
|
176
|
+
| `enums.ts` | TypeScript enums |
|
|
177
|
+
| `models.ts` | Model types and payload types |
|
|
178
|
+
| `inputs.ts` | Where, Create, Update, OrderBy input types and filter types |
|
|
179
|
+
| `args.ts` | Per-model operation argument types (FindManyArgs, CreateArgs, etc.) |
|
|
180
|
+
| `result.ts` | Type narrowing logic for select/include/omit |
|
|
181
|
+
| `delegates.ts` | Per-model delegate types with JSDoc examples |
|
|
182
|
+
| `schemas.ts` | Zod v4 validation schemas |
|
|
183
|
+
|
|
184
|
+
### `db push`
|
|
185
|
+
|
|
186
|
+
Push the schema directly to the database without creating migration files. Ideal for prototyping.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
bunx vibeorm db push # Safe changes only
|
|
190
|
+
bunx vibeorm db push --force # Allow destructive changes
|
|
191
|
+
bunx vibeorm db push --dry-run # Print SQL without executing
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
| Flag | Description |
|
|
195
|
+
|------|-------------|
|
|
196
|
+
| `--force` / `--accept-data-loss` | Allow destructive changes (drop columns, tables) |
|
|
197
|
+
| `--dry-run` | Print the DDL SQL without executing |
|
|
198
|
+
|
|
199
|
+
### `db pull`
|
|
200
|
+
|
|
201
|
+
Introspect an existing database and generate a `.prisma` schema file from it.
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
bunx vibeorm db pull # Write to schema file
|
|
205
|
+
bunx vibeorm db pull --print # Print to stdout instead
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### `db reset`
|
|
209
|
+
|
|
210
|
+
Drop the entire `public` schema, re-apply all migrations, and optionally run the seed script.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
bunx vibeorm db reset --force
|
|
214
|
+
bunx vibeorm db reset --force --skip-seed
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
| Flag | Description |
|
|
218
|
+
|------|-------------|
|
|
219
|
+
| `--force` | Required — confirms you want to drop everything |
|
|
220
|
+
| `--skip-seed` | Skip running the seed script after migrations |
|
|
221
|
+
|
|
222
|
+
### `db seed`
|
|
223
|
+
|
|
224
|
+
Run the configured seed script.
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
bunx vibeorm db seed
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Spawns `bun run <seedPath>` as a subprocess.
|
|
231
|
+
|
|
232
|
+
### `db execute`
|
|
233
|
+
|
|
234
|
+
Execute a raw SQL file against the database.
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
bunx vibeorm db execute --file ./scripts/cleanup.sql
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### `migrate generate`
|
|
241
|
+
|
|
242
|
+
Generate a new migration by diffing the current schema against the last snapshot. Does **not** require a database connection.
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
bunx vibeorm migrate generate --name add-posts-table
|
|
246
|
+
bunx vibeorm migrate generate --dry-run
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Creates `migrations/<timestamp>_<name>/migration.sql` + `down.sql` and updates the snapshot journal.
|
|
18
250
|
|
|
19
|
-
|
|
|
20
|
-
|
|
21
|
-
|
|
|
251
|
+
| Flag | Description |
|
|
252
|
+
|------|-------------|
|
|
253
|
+
| `--name <name>` | Migration name (default: `migration`) |
|
|
254
|
+
| `--dry-run` | Print the SQL without writing files |
|
|
22
255
|
|
|
23
|
-
###
|
|
256
|
+
### `migrate apply`
|
|
24
257
|
|
|
25
|
-
|
|
26
|
-
|---------|-------------|
|
|
27
|
-
| `db push` | Diff schema against database and apply changes |
|
|
28
|
-
| `db pull` | Introspect database and write `.prisma` schema |
|
|
29
|
-
| `db reset` | Drop schema, re-apply migrations, optionally seed |
|
|
30
|
-
| `db seed` | Run configured seed script |
|
|
31
|
-
| `db execute` | Execute a raw SQL file |
|
|
258
|
+
Apply all pending migrations in order.
|
|
32
259
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
260
|
+
```bash
|
|
261
|
+
bunx vibeorm migrate apply
|
|
262
|
+
bunx vibeorm migrate apply --dry-run
|
|
263
|
+
```
|
|
37
264
|
|
|
38
|
-
###
|
|
265
|
+
### `migrate status`
|
|
39
266
|
|
|
40
|
-
|
|
41
|
-
|---------|-------------|
|
|
42
|
-
| `migrate generate` | Create migration SQL from schema diff |
|
|
43
|
-
| `migrate apply` | Apply pending migrations |
|
|
44
|
-
| `migrate status` | Show applied vs pending migrations |
|
|
45
|
-
| `migrate resolve` | Repair migration state |
|
|
267
|
+
Show which migrations are applied, pending, or modified.
|
|
46
268
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
269
|
+
```bash
|
|
270
|
+
bunx vibeorm migrate status
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### `migrate rollback`
|
|
274
|
+
|
|
275
|
+
Roll back the last N applied migrations using their `down.sql` files.
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
bunx vibeorm migrate rollback # Roll back 1
|
|
279
|
+
bunx vibeorm migrate rollback --steps 3 # Roll back 3
|
|
280
|
+
bunx vibeorm migrate rollback --dry-run
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### `migrate resolve`
|
|
284
|
+
|
|
285
|
+
Repair migration state when things get out of sync.
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
bunx vibeorm migrate resolve --applied my-migration # Mark as applied without running
|
|
289
|
+
bunx vibeorm migrate resolve --rolled-back my-migration # Remove from applied records
|
|
290
|
+
```
|
|
50
291
|
|
|
51
292
|
### Global Options
|
|
52
293
|
|
|
53
|
-
All commands accept:
|
|
294
|
+
All commands accept these flags:
|
|
295
|
+
|
|
296
|
+
| Flag | Description |
|
|
297
|
+
|------|-------------|
|
|
298
|
+
| `--schema <path>` | Path to `.prisma` schema file or directory |
|
|
299
|
+
| `--output <path>` | Output directory for generated files |
|
|
300
|
+
| `--migrations <path>` | Migrations directory |
|
|
301
|
+
| `--seed <path>` | Seed script path |
|
|
302
|
+
| `--url <url>` | Database connection string (overrides `DATABASE_URL` env) |
|
|
54
303
|
|
|
55
|
-
|
|
56
|
-
- `--output <path>` — output directory for generated files
|
|
57
|
-
- `--migrations <path>` — migrations directory
|
|
58
|
-
- `--seed <path>` — seed script path
|
|
59
|
-
- `--url <postgres-url>` — database connection string
|
|
304
|
+
---
|
|
60
305
|
|
|
61
306
|
## Configuration
|
|
62
307
|
|
|
63
|
-
Config
|
|
308
|
+
Config is resolved in this order (last wins):
|
|
64
309
|
|
|
65
|
-
1. Defaults
|
|
66
|
-
2.
|
|
67
|
-
3.
|
|
68
|
-
4. CLI flags
|
|
310
|
+
1. **Defaults** — `./prisma/schema.prisma`, `./generated/vibeorm`, `./migrations`
|
|
311
|
+
2. **`package.json`** — `vibeorm` key
|
|
312
|
+
3. **`vibeorm.config.ts`** — config file in project root
|
|
313
|
+
4. **CLI flags** — `--schema`, `--output`, `--migrations`, `--seed`
|
|
69
314
|
|
|
70
315
|
### package.json
|
|
71
316
|
|
|
@@ -91,6 +336,815 @@ export const config = {
|
|
|
91
336
|
};
|
|
92
337
|
```
|
|
93
338
|
|
|
339
|
+
### Typical migration workflow
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
# 1. Edit your schema
|
|
343
|
+
# 2. Generate a migration
|
|
344
|
+
bunx vibeorm migrate generate --name add-comments
|
|
345
|
+
|
|
346
|
+
# 3. Review the generated SQL in migrations/<timestamp>_add-comments/migration.sql
|
|
347
|
+
|
|
348
|
+
# 4. Apply pending migrations
|
|
349
|
+
bunx vibeorm migrate apply
|
|
350
|
+
|
|
351
|
+
# 5. Regenerate the client
|
|
352
|
+
bunx vibeorm generate
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Prototyping workflow (no migrations)
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
bunx vibeorm db push --force
|
|
359
|
+
bunx vibeorm generate
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Client API
|
|
365
|
+
|
|
366
|
+
### Instantiation
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import { VibeClient } from "./generated/vibeorm/index.ts";
|
|
370
|
+
import { bunAdapter } from "@vibeorm/adapter-bun";
|
|
371
|
+
|
|
372
|
+
const db = VibeClient({
|
|
373
|
+
// Required
|
|
374
|
+
adapter: bunAdapter({ url: "postgres://..." }),
|
|
375
|
+
// Or let it read DATABASE_URL from env:
|
|
376
|
+
// adapter: bunAdapter(),
|
|
377
|
+
|
|
378
|
+
// Optional
|
|
379
|
+
log: "query", // false | true | "query" | "info" | "warn" | "error"
|
|
380
|
+
relationStrategy: "query", // "query" (batched WHERE IN) | "join" (LATERAL JOIN)
|
|
381
|
+
countStrategy: "direct", // "direct" | "subquery"
|
|
382
|
+
eager: true, // Warm up connection pool immediately
|
|
383
|
+
validate: false, // false | true | "input" | "output" | "all"
|
|
384
|
+
defaultOrderByPk: false, // Add ORDER BY pk to all queries without explicit orderBy
|
|
385
|
+
debug: true, // false | true | ((params: { profile }) => void)
|
|
386
|
+
idGenerator: ({ model, field, defaultKind }) => {
|
|
387
|
+
return `${model.toLowerCase().slice(0, 3)}_${crypto.randomUUID()}`;
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
With `node-postgres`:
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
import { pgAdapter } from "@vibeorm/adapter-pg";
|
|
396
|
+
|
|
397
|
+
const db = VibeClient({
|
|
398
|
+
adapter: pgAdapter({ connectionString: "postgres://..." }),
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Connection management
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
await db.$connect(); // Explicitly warm up the connection pool
|
|
406
|
+
await db.$disconnect(); // Close all connections
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
### Read Operations
|
|
412
|
+
|
|
413
|
+
#### `findMany`
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
const users = await db.user.findMany({
|
|
417
|
+
where: { email: { endsWith: "@example.com" } },
|
|
418
|
+
include: { posts: true },
|
|
419
|
+
orderBy: { email: "asc" },
|
|
420
|
+
take: 20,
|
|
421
|
+
skip: 0,
|
|
422
|
+
});
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
With all options:
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
const users = await db.user.findMany({
|
|
429
|
+
where: { ... },
|
|
430
|
+
select: { id: true, email: true }, // Only these fields
|
|
431
|
+
include: { posts: true }, // Load relations (alongside all scalars)
|
|
432
|
+
omit: { createdAt: true }, // Exclude specific scalars
|
|
433
|
+
orderBy: [{ role: "desc" }, { name: "asc" }],
|
|
434
|
+
take: 20,
|
|
435
|
+
skip: 0,
|
|
436
|
+
cursor: { id: 10 }, // Cursor-based pagination
|
|
437
|
+
distinct: ["role"], // DISTINCT ON
|
|
438
|
+
relationStrategy: "join", // Per-query override
|
|
439
|
+
});
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
#### `findFirst`
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
const user = await db.user.findFirst({
|
|
446
|
+
where: { role: "ADMIN" },
|
|
447
|
+
orderBy: { createdAt: "desc" },
|
|
448
|
+
});
|
|
449
|
+
// Returns: User | null
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
#### `findUnique`
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
const user = await db.user.findUnique({
|
|
456
|
+
where: { id: 1 },
|
|
457
|
+
include: { posts: true },
|
|
458
|
+
});
|
|
459
|
+
// Returns: User | null
|
|
460
|
+
|
|
461
|
+
// Compound unique:
|
|
462
|
+
const follow = await db.follow.findUnique({
|
|
463
|
+
where: { followerId_followingId: { followerId: 1, followingId: 2 } },
|
|
464
|
+
});
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
#### `findFirstOrThrow` / `findUniqueOrThrow`
|
|
468
|
+
|
|
469
|
+
Same as `findFirst`/`findUnique` but throw `VibeRequestError` with code `"NOT_FOUND"` if no match.
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
const user = await db.user.findUniqueOrThrow({
|
|
473
|
+
where: { id: 1 },
|
|
474
|
+
});
|
|
475
|
+
// Throws if not found — return type is never null
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
### Write Operations
|
|
481
|
+
|
|
482
|
+
#### `create`
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
const user = await db.user.create({
|
|
486
|
+
data: {
|
|
487
|
+
email: "alice@example.com",
|
|
488
|
+
name: "Alice",
|
|
489
|
+
},
|
|
490
|
+
select: { id: true, email: true }, // Narrow the return type
|
|
491
|
+
});
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
With nested create:
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
const user = await db.user.create({
|
|
498
|
+
data: {
|
|
499
|
+
email: "alice@example.com",
|
|
500
|
+
name: "Alice",
|
|
501
|
+
posts: {
|
|
502
|
+
create: [
|
|
503
|
+
{ title: "First Post", content: "Hello world" },
|
|
504
|
+
{ title: "Second Post" },
|
|
505
|
+
],
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
include: { posts: true },
|
|
509
|
+
});
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
#### `createMany`
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
const result = await db.user.createMany({
|
|
516
|
+
data: [
|
|
517
|
+
{ email: "alice@example.com" },
|
|
518
|
+
{ email: "bob@example.com" },
|
|
519
|
+
],
|
|
520
|
+
skipDuplicates: true, // ON CONFLICT DO NOTHING
|
|
521
|
+
});
|
|
522
|
+
// Returns: { count: number }
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
#### `createManyAndReturn`
|
|
526
|
+
|
|
527
|
+
```ts
|
|
528
|
+
const users = await db.user.createManyAndReturn({
|
|
529
|
+
data: [
|
|
530
|
+
{ email: "alice@example.com" },
|
|
531
|
+
{ email: "bob@example.com" },
|
|
532
|
+
],
|
|
533
|
+
select: { id: true, email: true },
|
|
534
|
+
});
|
|
535
|
+
// Returns: Array of created records
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
#### `update`
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
const user = await db.user.update({
|
|
542
|
+
where: { id: 1 },
|
|
543
|
+
data: { name: "Updated Name" },
|
|
544
|
+
select: { id: true, name: true },
|
|
545
|
+
});
|
|
546
|
+
// Throws NOT_FOUND if where matches nothing
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Atomic operations on numeric fields:
|
|
550
|
+
|
|
551
|
+
```ts
|
|
552
|
+
await db.post.update({
|
|
553
|
+
where: { id: 1 },
|
|
554
|
+
data: {
|
|
555
|
+
viewCount: { increment: 1 },
|
|
556
|
+
// Also: decrement, multiply, divide, set
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
#### `updateMany`
|
|
562
|
+
|
|
563
|
+
```ts
|
|
564
|
+
const result = await db.user.updateMany({
|
|
565
|
+
where: { role: "USER" },
|
|
566
|
+
data: { role: "ADMIN" },
|
|
567
|
+
});
|
|
568
|
+
// Returns: { count: number }
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
#### `upsert`
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
const user = await db.user.upsert({
|
|
575
|
+
where: { email: "alice@example.com" },
|
|
576
|
+
create: { email: "alice@example.com", name: "Alice" },
|
|
577
|
+
update: { name: "Alice Updated" },
|
|
578
|
+
});
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
#### `delete`
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
const user = await db.user.delete({
|
|
585
|
+
where: { id: 1 },
|
|
586
|
+
select: { id: true, email: true },
|
|
587
|
+
});
|
|
588
|
+
// Throws NOT_FOUND if where matches nothing
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
#### `deleteMany`
|
|
592
|
+
|
|
593
|
+
```ts
|
|
594
|
+
const result = await db.user.deleteMany({
|
|
595
|
+
where: { role: "USER" },
|
|
596
|
+
});
|
|
597
|
+
// Returns: { count: number }
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
### Nested Writes
|
|
603
|
+
|
|
604
|
+
Nested operations are available inside `create` and `update` data:
|
|
605
|
+
|
|
606
|
+
```ts
|
|
607
|
+
// In create
|
|
608
|
+
await db.user.create({
|
|
609
|
+
data: {
|
|
610
|
+
email: "alice@example.com",
|
|
611
|
+
posts: {
|
|
612
|
+
create: { title: "New Post" },
|
|
613
|
+
connect: [{ id: 5 }],
|
|
614
|
+
connectOrCreate: {
|
|
615
|
+
where: { id: 10 },
|
|
616
|
+
create: { title: "If not exists" },
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
profile: {
|
|
620
|
+
create: { bio: "Hello" },
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// In update
|
|
626
|
+
await db.user.update({
|
|
627
|
+
where: { id: 1 },
|
|
628
|
+
data: {
|
|
629
|
+
posts: {
|
|
630
|
+
create: { title: "New Post" },
|
|
631
|
+
connect: [{ id: 5 }],
|
|
632
|
+
disconnect: [{ id: 3 }],
|
|
633
|
+
delete: [{ id: 2 }],
|
|
634
|
+
set: [{ id: 5 }, { id: 6 }], // Replace all
|
|
635
|
+
update: [{ where: { id: 5 }, data: { title: "Updated" } }],
|
|
636
|
+
upsert: [{ where: { id: 5 }, create: { title: "New" }, update: { title: "Upd" } }],
|
|
637
|
+
updateMany: [{ where: { published: false }, data: { published: true } }],
|
|
638
|
+
deleteMany: [{ authorId: 1 }],
|
|
639
|
+
connectOrCreate: { where: { id: 10 }, create: { title: "Created" } },
|
|
640
|
+
},
|
|
641
|
+
profile: {
|
|
642
|
+
create: { bio: "New" },
|
|
643
|
+
update: { bio: "Updated" },
|
|
644
|
+
upsert: { create: { bio: "New" }, update: { bio: "Updated" } },
|
|
645
|
+
connect: { id: 5 },
|
|
646
|
+
disconnect: true,
|
|
647
|
+
delete: true,
|
|
648
|
+
connectOrCreate: { where: { userId: 1 }, create: { bio: "Hello" } },
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
### Where Filters
|
|
657
|
+
|
|
658
|
+
#### Scalar filters
|
|
659
|
+
|
|
660
|
+
```ts
|
|
661
|
+
where: {
|
|
662
|
+
// Equality (shorthand)
|
|
663
|
+
email: "alice@example.com",
|
|
664
|
+
|
|
665
|
+
// Explicit operators
|
|
666
|
+
email: { equals: "alice@example.com" },
|
|
667
|
+
email: { not: "alice@example.com" },
|
|
668
|
+
email: { in: ["alice@example.com", "bob@example.com"] },
|
|
669
|
+
email: { notIn: ["alice@example.com"] },
|
|
670
|
+
|
|
671
|
+
// String filters
|
|
672
|
+
email: { contains: "alice" },
|
|
673
|
+
email: { startsWith: "alice" },
|
|
674
|
+
email: { endsWith: "@example.com" },
|
|
675
|
+
email: { contains: "alice", mode: "insensitive" }, // Case-insensitive
|
|
676
|
+
|
|
677
|
+
// Numeric / DateTime comparisons
|
|
678
|
+
id: { lt: 10 },
|
|
679
|
+
id: { lte: 10 },
|
|
680
|
+
id: { gt: 5 },
|
|
681
|
+
id: { gte: 5 },
|
|
682
|
+
|
|
683
|
+
// Null checks
|
|
684
|
+
name: null, // IS NULL
|
|
685
|
+
name: { not: null }, // IS NOT NULL
|
|
686
|
+
|
|
687
|
+
// Boolean
|
|
688
|
+
published: true,
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
#### Logical combinators
|
|
693
|
+
|
|
694
|
+
```ts
|
|
695
|
+
where: {
|
|
696
|
+
AND: [{ role: "ADMIN" }, { name: { not: null } }],
|
|
697
|
+
OR: [{ role: "ADMIN" }, { role: "MODERATOR" }],
|
|
698
|
+
NOT: { role: "USER" },
|
|
699
|
+
NOT: [{ role: "USER" }, { email: { contains: "test" } }],
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
#### Relation filters
|
|
704
|
+
|
|
705
|
+
```ts
|
|
706
|
+
where: {
|
|
707
|
+
// To-many relations
|
|
708
|
+
posts: {
|
|
709
|
+
some: { published: true }, // At least one post is published
|
|
710
|
+
none: { published: false }, // No posts are unpublished
|
|
711
|
+
every: { published: true }, // All posts are published
|
|
712
|
+
},
|
|
713
|
+
|
|
714
|
+
// To-one relations
|
|
715
|
+
author: {
|
|
716
|
+
is: { role: "ADMIN" },
|
|
717
|
+
isNot: { role: "USER" },
|
|
718
|
+
is: null, // FK IS NULL (no related record)
|
|
719
|
+
},
|
|
720
|
+
|
|
721
|
+
// Shorthand — object is treated as "is" (to-one) or "some" (to-many)
|
|
722
|
+
author: { role: "ADMIN" },
|
|
723
|
+
posts: { published: true },
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
#### JSON filters
|
|
728
|
+
|
|
729
|
+
```ts
|
|
730
|
+
where: {
|
|
731
|
+
metadata: { equals: { key: "value" } },
|
|
732
|
+
metadata: { path: ["nested", "key"], equals: "value" },
|
|
733
|
+
metadata: { path: ["arr"], array_contains: [1, 2] },
|
|
734
|
+
metadata: { path: ["arr"], array_starts_with: [1] },
|
|
735
|
+
metadata: { path: ["arr"], array_ends_with: [3] },
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
#### Scalar list (array) filters
|
|
740
|
+
|
|
741
|
+
```ts
|
|
742
|
+
where: {
|
|
743
|
+
tags: { has: "typescript" },
|
|
744
|
+
tags: { hasEvery: ["typescript", "orm"] },
|
|
745
|
+
tags: { hasSome: ["typescript", "javascript"] },
|
|
746
|
+
tags: { isEmpty: true },
|
|
747
|
+
tags: { equals: ["typescript", "orm"] },
|
|
748
|
+
}
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
### Select, Include, and Omit
|
|
754
|
+
|
|
755
|
+
These options control which fields and relations are returned. **The return type is narrowed at compile time** — you get exact types for every query.
|
|
756
|
+
|
|
757
|
+
#### `select` — return only specified fields
|
|
758
|
+
|
|
759
|
+
```ts
|
|
760
|
+
const users = await db.user.findMany({
|
|
761
|
+
select: {
|
|
762
|
+
id: true,
|
|
763
|
+
email: true,
|
|
764
|
+
posts: {
|
|
765
|
+
select: { title: true, published: true },
|
|
766
|
+
where: { published: true },
|
|
767
|
+
take: 5,
|
|
768
|
+
},
|
|
769
|
+
_count: { select: { posts: true } },
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
// Type: { id: number; email: string; posts: { title: string; published: boolean }[]; _count: { posts: number } }[]
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
#### `include` — load relations alongside all scalar fields
|
|
776
|
+
|
|
777
|
+
```ts
|
|
778
|
+
const users = await db.user.findMany({
|
|
779
|
+
include: {
|
|
780
|
+
posts: true, // All posts
|
|
781
|
+
posts: { // With filtering/pagination
|
|
782
|
+
where: { published: true },
|
|
783
|
+
orderBy: { createdAt: "desc" },
|
|
784
|
+
take: 5,
|
|
785
|
+
include: { comments: true }, // Nested include
|
|
786
|
+
},
|
|
787
|
+
profile: true,
|
|
788
|
+
_count: true, // Count all list relations
|
|
789
|
+
_count: { select: { posts: true } }, // Count specific relations
|
|
790
|
+
},
|
|
791
|
+
});
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
#### `omit` — exclude specific scalar fields
|
|
795
|
+
|
|
796
|
+
```ts
|
|
797
|
+
const users = await db.user.findMany({
|
|
798
|
+
omit: { createdAt: true, updatedAt: true },
|
|
799
|
+
});
|
|
800
|
+
// Returns all scalar fields except createdAt and updatedAt
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
---
|
|
804
|
+
|
|
805
|
+
### OrderBy, Pagination, and Distinct
|
|
806
|
+
|
|
807
|
+
#### OrderBy
|
|
808
|
+
|
|
809
|
+
```ts
|
|
810
|
+
// Single field
|
|
811
|
+
orderBy: { createdAt: "desc" },
|
|
812
|
+
|
|
813
|
+
// Multiple fields
|
|
814
|
+
orderBy: [{ role: "desc" }, { name: "asc" }],
|
|
815
|
+
|
|
816
|
+
// Null ordering
|
|
817
|
+
orderBy: { name: { sort: "desc", nulls: "last" } },
|
|
818
|
+
|
|
819
|
+
// Relation field
|
|
820
|
+
orderBy: { author: { name: "asc" } },
|
|
821
|
+
|
|
822
|
+
// Relation count
|
|
823
|
+
orderBy: { posts: { _count: "desc" } },
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
#### Cursor pagination
|
|
827
|
+
|
|
828
|
+
```ts
|
|
829
|
+
// Forward pagination
|
|
830
|
+
const page = await db.post.findMany({
|
|
831
|
+
cursor: { id: lastSeenId },
|
|
832
|
+
take: 20,
|
|
833
|
+
orderBy: { id: "asc" },
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// Backward pagination (negative take)
|
|
837
|
+
const prevPage = await db.post.findMany({
|
|
838
|
+
cursor: { id: firstSeenId },
|
|
839
|
+
take: -20,
|
|
840
|
+
orderBy: { id: "asc" },
|
|
841
|
+
});
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
#### Offset pagination
|
|
845
|
+
|
|
846
|
+
```ts
|
|
847
|
+
const page = await db.post.findMany({
|
|
848
|
+
skip: 20,
|
|
849
|
+
take: 10,
|
|
850
|
+
orderBy: { createdAt: "desc" },
|
|
851
|
+
});
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
#### Distinct
|
|
855
|
+
|
|
856
|
+
```ts
|
|
857
|
+
const roles = await db.user.findMany({
|
|
858
|
+
distinct: ["role"],
|
|
859
|
+
select: { role: true },
|
|
860
|
+
});
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
---
|
|
864
|
+
|
|
865
|
+
### Aggregation
|
|
866
|
+
|
|
867
|
+
#### `count`
|
|
868
|
+
|
|
869
|
+
```ts
|
|
870
|
+
const total = await db.post.count({
|
|
871
|
+
where: { published: true },
|
|
872
|
+
});
|
|
873
|
+
// Returns: number
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
#### `aggregate`
|
|
877
|
+
|
|
878
|
+
```ts
|
|
879
|
+
const stats = await db.post.aggregate({
|
|
880
|
+
where: { published: true },
|
|
881
|
+
_count: true,
|
|
882
|
+
_avg: { viewCount: true },
|
|
883
|
+
_sum: { viewCount: true },
|
|
884
|
+
_min: { createdAt: true },
|
|
885
|
+
_max: { createdAt: true },
|
|
886
|
+
});
|
|
887
|
+
// Returns: { _count: number, _avg: { viewCount: number | null }, _sum: {...}, _min: {...}, _max: {...} }
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
#### `groupBy`
|
|
891
|
+
|
|
892
|
+
```ts
|
|
893
|
+
const grouped = await db.post.groupBy({
|
|
894
|
+
by: ["published", "authorId"],
|
|
895
|
+
where: { createdAt: { gte: new Date("2024-01-01") } },
|
|
896
|
+
_count: true,
|
|
897
|
+
_avg: { viewCount: true },
|
|
898
|
+
having: {
|
|
899
|
+
viewCount: { _avg: { gte: 10 } },
|
|
900
|
+
},
|
|
901
|
+
orderBy: { published: "desc" },
|
|
902
|
+
take: 10,
|
|
903
|
+
});
|
|
904
|
+
// Returns: Array of { published, authorId, _count, _avg: { viewCount } }
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
---
|
|
908
|
+
|
|
909
|
+
### Raw SQL
|
|
910
|
+
|
|
911
|
+
#### Tagged template literals (parameterized)
|
|
912
|
+
|
|
913
|
+
```ts
|
|
914
|
+
const admins = await db.$queryRaw<{ id: number; email: string }>`
|
|
915
|
+
SELECT id, email FROM "User" WHERE role = ${"ADMIN"}
|
|
916
|
+
`;
|
|
917
|
+
|
|
918
|
+
const affected = await db.$executeRaw`
|
|
919
|
+
UPDATE "Post" SET published = true WHERE "authorId" = ${1}
|
|
920
|
+
`;
|
|
921
|
+
// Returns: number (affected row count)
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
#### Unsafe variants (string + params)
|
|
925
|
+
|
|
926
|
+
```ts
|
|
927
|
+
const rows = await db.$queryRawUnsafe<{ count: number }>(
|
|
928
|
+
'SELECT COUNT(*) as count FROM "User" WHERE role = $1',
|
|
929
|
+
"ADMIN"
|
|
930
|
+
);
|
|
931
|
+
|
|
932
|
+
const affected = await db.$executeRawUnsafe(
|
|
933
|
+
'DELETE FROM "User" WHERE id = $1',
|
|
934
|
+
123
|
|
935
|
+
);
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
---
|
|
939
|
+
|
|
940
|
+
### Transactions
|
|
941
|
+
|
|
942
|
+
#### Callback style
|
|
943
|
+
|
|
944
|
+
```ts
|
|
945
|
+
const result = await db.$transaction(async (tx) => {
|
|
946
|
+
const user = await tx.user.create({
|
|
947
|
+
data: { email: "alice@example.com" },
|
|
948
|
+
});
|
|
949
|
+
await tx.post.create({
|
|
950
|
+
data: { title: "Hello", authorId: user.id },
|
|
951
|
+
});
|
|
952
|
+
return user;
|
|
953
|
+
}, {
|
|
954
|
+
isolationLevel: "Serializable", // "ReadCommitted" | "RepeatableRead" | "Serializable"
|
|
955
|
+
timeout: 5000, // ms
|
|
956
|
+
});
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
#### Array style
|
|
960
|
+
|
|
961
|
+
```ts
|
|
962
|
+
const [user, posts] = await db.$transaction([
|
|
963
|
+
db.user.create({ data: { email: "alice@example.com" } }),
|
|
964
|
+
db.post.findMany({ where: { published: true } }),
|
|
965
|
+
]);
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
---
|
|
969
|
+
|
|
970
|
+
### Relation Loading Strategies
|
|
971
|
+
|
|
972
|
+
| Strategy | Behavior | Best for |
|
|
973
|
+
|----------|----------|----------|
|
|
974
|
+
| `"query"` (default) | Parent query + batched `WHERE IN` relation queries. Sibling relations load in parallel. | Deep/nested includes |
|
|
975
|
+
| `"join"` | Single query with `LEFT JOIN LATERAL` + JSON aggregation | Flat includes, fewer round-trips |
|
|
976
|
+
|
|
977
|
+
Set globally:
|
|
978
|
+
|
|
979
|
+
```ts
|
|
980
|
+
const db = VibeClient({
|
|
981
|
+
adapter: bunAdapter(),
|
|
982
|
+
relationStrategy: "join",
|
|
983
|
+
});
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
Or per-query:
|
|
987
|
+
|
|
988
|
+
```ts
|
|
989
|
+
const users = await db.user.findMany({
|
|
990
|
+
include: { posts: true },
|
|
991
|
+
relationStrategy: "join",
|
|
992
|
+
});
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
---
|
|
996
|
+
|
|
997
|
+
## Error Handling
|
|
998
|
+
|
|
999
|
+
VibeORM provides structured error classes with codes and metadata:
|
|
1000
|
+
|
|
1001
|
+
```
|
|
1002
|
+
VibeError (base)
|
|
1003
|
+
VibeRequestError (deterministic / data errors)
|
|
1004
|
+
VibeValidationError (Zod validation failures)
|
|
1005
|
+
VibeTransientError (retryable infrastructure errors)
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
### Error codes
|
|
1009
|
+
|
|
1010
|
+
| Class | Code | Meaning |
|
|
1011
|
+
|-------|------|---------|
|
|
1012
|
+
| `VibeRequestError` | `UNIQUE_CONSTRAINT` | Duplicate value on unique field |
|
|
1013
|
+
| | `FOREIGN_KEY_VIOLATION` | FK reference doesn't exist |
|
|
1014
|
+
| | `NOT_NULL_VIOLATION` | Required field is null |
|
|
1015
|
+
| | `CHECK_CONSTRAINT` | Check constraint failed |
|
|
1016
|
+
| | `NOT_FOUND` | `findUniqueOrThrow`/`update`/`delete` found no match |
|
|
1017
|
+
| | `VALIDATION_ERROR` | Zod schema validation failed |
|
|
1018
|
+
| `VibeTransientError` | `CONNECTION_ERROR` | Can't reach database |
|
|
1019
|
+
| | `DEADLOCK` | Transaction deadlock |
|
|
1020
|
+
| | `SERIALIZATION_FAILURE` | Serializable isolation conflict |
|
|
1021
|
+
| | `STATEMENT_TIMEOUT` | Query exceeded timeout |
|
|
1022
|
+
| | `TOO_MANY_CONNECTIONS` | Pool exhausted |
|
|
1023
|
+
|
|
1024
|
+
### Example
|
|
1025
|
+
|
|
1026
|
+
```ts
|
|
1027
|
+
import { VibeRequestError, VibeTransientError } from "@vibeorm/runtime";
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
1030
|
+
await db.user.create({ data: { email: "taken@example.com" } });
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
if (error instanceof VibeRequestError) {
|
|
1033
|
+
if (error.code === "UNIQUE_CONSTRAINT") {
|
|
1034
|
+
console.log(error.meta.constraint); // "User_email_key"
|
|
1035
|
+
console.log(error.meta.detail); // 'Key (email)=(taken@...) already exists.'
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (error instanceof VibeTransientError) {
|
|
1039
|
+
// error.retryable === true — safe to retry
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
---
|
|
1045
|
+
|
|
1046
|
+
## Validation
|
|
1047
|
+
|
|
1048
|
+
The generated client includes Zod v4 schemas for every model. Enable runtime validation with the `validate` option:
|
|
1049
|
+
|
|
1050
|
+
```ts
|
|
1051
|
+
const db = VibeClient({
|
|
1052
|
+
adapter: bunAdapter(),
|
|
1053
|
+
validate: true,
|
|
1054
|
+
});
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
| Value | Behavior |
|
|
1058
|
+
|-------|----------|
|
|
1059
|
+
| `false` (default) | No validation, zero overhead |
|
|
1060
|
+
| `"input"` | Validate `data` on create/update/upsert |
|
|
1061
|
+
| `"output"` | Validate every returned record |
|
|
1062
|
+
| `true` / `"all"` | Both input and output |
|
|
1063
|
+
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
## Adapters
|
|
1067
|
+
|
|
1068
|
+
### `@vibeorm/adapter-bun` (recommended)
|
|
1069
|
+
|
|
1070
|
+
Uses Bun's built-in `bun:sql` PostgreSQL driver. Zero external dependencies.
|
|
1071
|
+
|
|
1072
|
+
```ts
|
|
1073
|
+
import { bunAdapter } from "@vibeorm/adapter-bun";
|
|
1074
|
+
|
|
1075
|
+
const db = VibeClient({
|
|
1076
|
+
adapter: bunAdapter({
|
|
1077
|
+
url: "postgres://...", // Or reads DATABASE_URL
|
|
1078
|
+
}),
|
|
1079
|
+
});
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
### `@vibeorm/adapter-pg`
|
|
1083
|
+
|
|
1084
|
+
Uses `node-postgres` (`pg`). Install separately:
|
|
1085
|
+
|
|
1086
|
+
```bash
|
|
1087
|
+
bun add @vibeorm/adapter-pg pg
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
```ts
|
|
1091
|
+
import { pgAdapter } from "@vibeorm/adapter-pg";
|
|
1092
|
+
|
|
1093
|
+
const db = VibeClient({
|
|
1094
|
+
adapter: pgAdapter({
|
|
1095
|
+
connectionString: "postgres://...",
|
|
1096
|
+
}),
|
|
1097
|
+
});
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
---
|
|
1101
|
+
|
|
1102
|
+
## ID Generation
|
|
1103
|
+
|
|
1104
|
+
Built-in cryptographically secure generators for schema-defined defaults:
|
|
1105
|
+
|
|
1106
|
+
| Default | Format | Example |
|
|
1107
|
+
|---------|--------|---------|
|
|
1108
|
+
| `@default(uuid())` | UUID v4 | `550e8400-e29b-41d4-a716-446655440000` |
|
|
1109
|
+
| `@default(cuid())` | CUID2 (24 chars) | `clx1234abcde567890fghij` |
|
|
1110
|
+
| `@default(nanoid())` | NanoID (21 chars) | `V1StGXR8_Z5jdHi6B-myT` |
|
|
1111
|
+
| `@default(ulid())` | ULID (26 chars, sortable) | `01ARZ3NDEKTSV4RRFFQ69G5FAV` |
|
|
1112
|
+
|
|
1113
|
+
Custom ID prefixes via Prisma doc comments:
|
|
1114
|
+
|
|
1115
|
+
```prisma
|
|
1116
|
+
model User {
|
|
1117
|
+
/// @vibeorm.idPrefix("usr_")
|
|
1118
|
+
id String @id @default(cuid())
|
|
1119
|
+
}
|
|
1120
|
+
// Generates IDs like: "usr_clx1234abcde567890fghij"
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
Override globally:
|
|
1124
|
+
|
|
1125
|
+
```ts
|
|
1126
|
+
const db = VibeClient({
|
|
1127
|
+
adapter: bunAdapter(),
|
|
1128
|
+
idGenerator: ({ model, field, defaultKind }) => {
|
|
1129
|
+
return `${model.toLowerCase().slice(0, 3)}_${crypto.randomUUID()}`;
|
|
1130
|
+
},
|
|
1131
|
+
});
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
---
|
|
1135
|
+
|
|
1136
|
+
## Packages
|
|
1137
|
+
|
|
1138
|
+
| Package | npm | Purpose |
|
|
1139
|
+
|---------|-----|---------|
|
|
1140
|
+
| `vibeorm` | [`vibeorm`](https://www.npmjs.com/package/vibeorm) | CLI |
|
|
1141
|
+
| `@vibeorm/runtime` | [`@vibeorm/runtime`](https://www.npmjs.com/package/@vibeorm/runtime) | Query engine and client runtime |
|
|
1142
|
+
| `@vibeorm/adapter-bun` | [`@vibeorm/adapter-bun`](https://www.npmjs.com/package/@vibeorm/adapter-bun) | `bun:sql` adapter |
|
|
1143
|
+
| `@vibeorm/adapter-pg` | [`@vibeorm/adapter-pg`](https://www.npmjs.com/package/@vibeorm/adapter-pg) | `node-postgres` adapter |
|
|
1144
|
+
| `@vibeorm/parser` | [`@vibeorm/parser`](https://www.npmjs.com/package/@vibeorm/parser) | Prisma schema parser |
|
|
1145
|
+
| `@vibeorm/generator` | [`@vibeorm/generator`](https://www.npmjs.com/package/@vibeorm/generator) | TypeScript client generator |
|
|
1146
|
+
| `@vibeorm/migrate` | [`@vibeorm/migrate`](https://www.npmjs.com/package/@vibeorm/migrate) | Migration and introspection toolkit |
|
|
1147
|
+
|
|
94
1148
|
## License
|
|
95
1149
|
|
|
96
1150
|
[MIT](../../LICENSE)
|