vibeorm 1.1.1 → 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 CHANGED
@@ -1,71 +1,316 @@
1
- # vibeorm
1
+ # VibeORM
2
2
 
3
- CLI for VibeORM generate clients, run migrations, and manage your PostgreSQL database.
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
- ## Installation
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
- bun add -d vibeorm
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
- ### Code Generation
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
- | Command | Description |
20
- |---------|-------------|
21
- | `generate` | Parse `.prisma` schema and generate TypeScript client files |
251
+ | Flag | Description |
252
+ |------|-------------|
253
+ | `--name <name>` | Migration name (default: `migration`) |
254
+ | `--dry-run` | Print the SQL without writing files |
22
255
 
23
- ### Database Management
256
+ ### `migrate apply`
24
257
 
25
- | Command | Description |
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
- Options for `db push`: `--force`, `--accept-data-loss`, `--dry-run`
34
- Options for `db pull`: `--print`
35
- Options for `db reset`: `--force` (required), `--skip-seed`
36
- Options for `db execute`: `--file <path>`
260
+ ```bash
261
+ bunx vibeorm migrate apply
262
+ bunx vibeorm migrate apply --dry-run
263
+ ```
37
264
 
38
- ### Migrations
265
+ ### `migrate status`
39
266
 
40
- | Command | Description |
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
- Options for `migrate generate`: `--name <name>`, `--dry-run`
48
- Options for `migrate apply`: `--dry-run`
49
- Options for `migrate resolve`: `--applied <name>`, `--rolled-back <name>`
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
- - `--schema <path>` — path to `.prisma` schema file or directory
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 priority (last wins):
308
+ Config is resolved in this order (last wins):
64
309
 
65
- 1. Defaults (`./prisma/schema.prisma`, `./generated/vibeorm`, `./migrations`)
66
- 2. `package.json` `vibeorm` key
67
- 3. `vibeorm.config.ts`
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)