Initial commit
This commit is contained in:
642
references/prisma-integration.md
Normal file
642
references/prisma-integration.md
Normal file
@@ -0,0 +1,642 @@
|
||||
# Prisma ORM Integration Guide
|
||||
|
||||
Complete guide to using Prisma ORM with Cloudflare Hyperdrive.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
**Prisma ORM** is a popular Node.js and TypeScript ORM focused on type safety and developer experience.
|
||||
|
||||
**Why Prisma + Hyperdrive?**
|
||||
- ✅ Excellent TypeScript support and auto-completion
|
||||
- ✅ Powerful migrations and schema management
|
||||
- ✅ Intuitive API with `.findMany()`, `.create()`, etc.
|
||||
- ✅ Works with Hyperdrive via driver adapters
|
||||
|
||||
**CRITICAL**: Prisma requires **driver adapters** (`@prisma/adapter-pg`) to work with Hyperdrive.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Prisma CLI and client
|
||||
npm install prisma @prisma/client
|
||||
|
||||
# PostgreSQL driver and adapter
|
||||
npm install pg @prisma/adapter-pg
|
||||
|
||||
# TypeScript types for pg
|
||||
npm install -D @types/pg
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Initialize Prisma
|
||||
|
||||
```bash
|
||||
npx prisma init
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `prisma/` directory
|
||||
- `prisma/schema.prisma` file
|
||||
- `.env` file
|
||||
|
||||
---
|
||||
|
||||
### 2. Configure Schema
|
||||
|
||||
Edit `prisma/schema.prisma`:
|
||||
|
||||
```prisma
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["driverAdapters"] // REQUIRED for Hyperdrive
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
email String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
posts Post[]
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
content String?
|
||||
published Boolean @default(false)
|
||||
authorId Int
|
||||
author User @relation(fields: [authorId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Set Database URL
|
||||
|
||||
Edit `.env` (for migrations only, NOT used in Worker):
|
||||
|
||||
```bash
|
||||
# Direct connection to database (for migrations)
|
||||
DATABASE_URL="postgres://user:password@host:5432/database"
|
||||
```
|
||||
|
||||
**Important**: This `.env` file is only for running migrations locally. Workers get connection string from Hyperdrive binding.
|
||||
|
||||
---
|
||||
|
||||
### 4. Generate Prisma Client
|
||||
|
||||
```bash
|
||||
npx prisma generate --no-engine
|
||||
```
|
||||
|
||||
**CRITICAL**: Use `--no-engine` flag for Workers compatibility.
|
||||
|
||||
This generates the Prisma Client in `node_modules/@prisma/client`.
|
||||
|
||||
---
|
||||
|
||||
### 5. Run Migrations
|
||||
|
||||
```bash
|
||||
# Create and apply migration
|
||||
npx prisma migrate dev --name init
|
||||
|
||||
# Or apply existing migrations
|
||||
npx prisma migrate deploy
|
||||
```
|
||||
|
||||
**Generated SQL** (in `prisma/migrations/` folder):
|
||||
```sql
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Post" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT,
|
||||
"published" BOOLEAN NOT NULL DEFAULT false,
|
||||
"authorId" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey"
|
||||
FOREIGN KEY ("authorId") REFERENCES "User"("id")
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use in Worker
|
||||
|
||||
```typescript
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { Pool } from "pg";
|
||||
|
||||
type Bindings = {
|
||||
HYPERDRIVE: Hyperdrive;
|
||||
};
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Bindings, ctx: ExecutionContext): Promise<Response> {
|
||||
// Create pg.Pool for driver adapter
|
||||
const pool = new Pool({
|
||||
connectionString: env.HYPERDRIVE.connectionString,
|
||||
max: 5 // CRITICAL: Workers limit is 6 concurrent connections
|
||||
});
|
||||
|
||||
// Create Prisma driver adapter
|
||||
const adapter = new PrismaPg(pool);
|
||||
|
||||
// Create Prisma client with adapter
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
try {
|
||||
// Create user
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
name: "John Doe",
|
||||
email: `john.${Date.now()}@example.com`
|
||||
}
|
||||
});
|
||||
|
||||
// Find all users
|
||||
const allUsers = await prisma.user.findMany();
|
||||
|
||||
// Find user by email
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: "john@example.com" }
|
||||
});
|
||||
|
||||
// Update user
|
||||
await prisma.user.update({
|
||||
where: { id: newUser.id },
|
||||
data: { name: "Jane Doe" }
|
||||
});
|
||||
|
||||
// Create post with relation
|
||||
await prisma.post.create({
|
||||
data: {
|
||||
title: "My First Post",
|
||||
content: "Hello World!",
|
||||
published: true,
|
||||
authorId: newUser.id
|
||||
}
|
||||
});
|
||||
|
||||
// Find users with posts (include relation)
|
||||
const usersWithPosts = await prisma.user.findMany({
|
||||
include: {
|
||||
posts: true
|
||||
}
|
||||
});
|
||||
|
||||
return Response.json({
|
||||
newUser,
|
||||
allUsers,
|
||||
user,
|
||||
usersWithPosts
|
||||
});
|
||||
} catch (error: any) {
|
||||
return Response.json({
|
||||
error: error.message
|
||||
}, { status: 500 });
|
||||
} finally {
|
||||
// Clean up pool connections
|
||||
ctx.waitUntil(pool.end());
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Query Patterns
|
||||
|
||||
### Create
|
||||
|
||||
```typescript
|
||||
// Create single record
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name: "John",
|
||||
email: "john@example.com"
|
||||
}
|
||||
});
|
||||
|
||||
// Create with relation
|
||||
const post = await prisma.post.create({
|
||||
data: {
|
||||
title: "Hello",
|
||||
content: "World",
|
||||
author: {
|
||||
connect: { id: userId }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create with nested relation
|
||||
const userWithPost = await prisma.user.create({
|
||||
data: {
|
||||
name: "John",
|
||||
email: "john@example.com",
|
||||
posts: {
|
||||
create: [
|
||||
{ title: "First Post", content: "Hello" }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Read
|
||||
|
||||
```typescript
|
||||
// Find all
|
||||
const users = await prisma.user.findMany();
|
||||
|
||||
// Find with filter
|
||||
const activeUsers = await prisma.user.findMany({
|
||||
where: { active: true }
|
||||
});
|
||||
|
||||
// Find unique
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: "john@example.com" }
|
||||
});
|
||||
|
||||
// Find first
|
||||
const firstUser = await prisma.user.findFirst({
|
||||
where: { name: "John" }
|
||||
});
|
||||
|
||||
// Find with relations
|
||||
const usersWithPosts = await prisma.user.findMany({
|
||||
include: {
|
||||
posts: true
|
||||
}
|
||||
});
|
||||
|
||||
// Pagination
|
||||
const users = await prisma.user.findMany({
|
||||
skip: 20,
|
||||
take: 10
|
||||
});
|
||||
|
||||
// Sorting
|
||||
const users = await prisma.user.findMany({
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Update
|
||||
|
||||
```typescript
|
||||
// Update one
|
||||
const user = await prisma.user.update({
|
||||
where: { id: 1 },
|
||||
data: { name: "Jane" }
|
||||
});
|
||||
|
||||
// Update many
|
||||
const result = await prisma.user.updateMany({
|
||||
where: { active: false },
|
||||
data: { deleted: true }
|
||||
});
|
||||
|
||||
// Upsert (update or insert)
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: "john@example.com" },
|
||||
update: { name: "John Updated" },
|
||||
create: { name: "John", email: "john@example.com" }
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Delete
|
||||
|
||||
```typescript
|
||||
// Delete one
|
||||
const user = await prisma.user.delete({
|
||||
where: { id: 1 }
|
||||
});
|
||||
|
||||
// Delete many
|
||||
const result = await prisma.user.deleteMany({
|
||||
where: { active: false }
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Aggregations
|
||||
|
||||
```typescript
|
||||
// Count
|
||||
const count = await prisma.user.count();
|
||||
|
||||
// Count with filter
|
||||
const activeCount = await prisma.user.count({
|
||||
where: { active: true }
|
||||
});
|
||||
|
||||
// Aggregate
|
||||
const result = await prisma.post.aggregate({
|
||||
_count: { id: true },
|
||||
_avg: { views: true },
|
||||
_sum: { views: true },
|
||||
_min: { createdAt: true },
|
||||
_max: { createdAt: true }
|
||||
});
|
||||
|
||||
// Group by
|
||||
const result = await prisma.post.groupBy({
|
||||
by: ['authorId'],
|
||||
_count: { id: true }
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Complex Filters
|
||||
|
||||
```typescript
|
||||
// AND
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{ active: true },
|
||||
{ role: 'admin' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// OR
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ role: 'admin' },
|
||||
{ role: 'moderator' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// NOT
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
NOT: {
|
||||
email: {
|
||||
endsWith: '@example.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Comparison operators
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: new Date('2024-01-01'),
|
||||
lt: new Date('2024-12-31')
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// String filters
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
email: {
|
||||
contains: '@gmail.com' // or startsWith, endsWith
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Transactions
|
||||
|
||||
```typescript
|
||||
// Sequential operations (default)
|
||||
const result = await prisma.$transaction([
|
||||
prisma.user.create({ data: { name: "John", email: "john@example.com" } }),
|
||||
prisma.post.create({ data: { title: "Hello", authorId: 1 } })
|
||||
]);
|
||||
|
||||
// Interactive transactions
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.create({
|
||||
data: { name: "John", email: "john@example.com" }
|
||||
});
|
||||
|
||||
const post = await tx.post.create({
|
||||
data: { title: "Hello", authorId: user.id }
|
||||
});
|
||||
|
||||
return { user, post };
|
||||
});
|
||||
|
||||
// Transaction with error handling
|
||||
try {
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.create({ data: { ... } });
|
||||
|
||||
if (someCondition) {
|
||||
throw new Error("Rollback transaction");
|
||||
}
|
||||
|
||||
return user;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Transaction failed:", error);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TypeScript Types
|
||||
|
||||
Prisma automatically generates TypeScript types:
|
||||
|
||||
```typescript
|
||||
import { User, Post, Prisma } from "@prisma/client";
|
||||
|
||||
// Model types
|
||||
const user: User = await prisma.user.findUnique({ where: { id: 1 } });
|
||||
|
||||
// Input types
|
||||
const createUserData: Prisma.UserCreateInput = {
|
||||
name: "John",
|
||||
email: "john@example.com"
|
||||
};
|
||||
|
||||
// Return types with relations
|
||||
type UserWithPosts = Prisma.UserGetPayload<{
|
||||
include: { posts: true }
|
||||
}>;
|
||||
|
||||
const user: UserWithPosts = await prisma.user.findUnique({
|
||||
where: { id: 1 },
|
||||
include: { posts: true }
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prisma Studio
|
||||
|
||||
View and edit database with Prisma Studio:
|
||||
|
||||
```bash
|
||||
npx prisma studio
|
||||
```
|
||||
|
||||
**Note**: Runs locally, connects to database directly (not via Hyperdrive).
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use driver adapters** (`@prisma/adapter-pg`) - REQUIRED for Hyperdrive
|
||||
2. **Generate with --no-engine** - Required for Workers compatibility
|
||||
3. **Set max: 5 for pg.Pool** - Stay within Workers' 6 connection limit
|
||||
4. **Use ctx.waitUntil()** for cleanup
|
||||
5. **Run migrations outside Worker** - Use `prisma migrate` locally
|
||||
6. **Version control migrations** in `prisma/migrations/` folder
|
||||
7. **Use `.env` for migrations only** - Not used in Worker runtime
|
||||
8. **Re-generate client** after schema changes: `npx prisma generate --no-engine`
|
||||
|
||||
---
|
||||
|
||||
## Prisma vs Drizzle
|
||||
|
||||
| Feature | Prisma | Drizzle |
|
||||
|---------|--------|---------|
|
||||
| **Type Safety** | ✅ Excellent | ✅ Excellent |
|
||||
| **Migrations** | ✅ Prisma Migrate (powerful) | ✅ Drizzle Kit (simpler) |
|
||||
| **API Style** | `.findMany()`, `.create()` | `.select().from()` (SQL-like) |
|
||||
| **Bundle Size** | ⚠️ Larger | ✅ Smaller |
|
||||
| **Workers Setup** | ⚠️ Needs adapters + --no-engine | ✅ Simpler setup |
|
||||
| **Learning Curve** | ⚠️ Steeper | ✅ Easier (if you know SQL) |
|
||||
| **Performance** | ✅ Good | ✅ Excellent |
|
||||
|
||||
**Recommendation**:
|
||||
- **Use Prisma** if you want powerful migrations and intuitive API
|
||||
- **Use Drizzle** if you want lighter bundle size and SQL-like queries
|
||||
|
||||
---
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Error: "PrismaClient is unable to run in this browser environment"
|
||||
|
||||
**Cause**: Prisma Client not generated with `--no-engine` flag.
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
npx prisma generate --no-engine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error: "Cannot find module '@prisma/client'"
|
||||
|
||||
**Cause**: Prisma Client not generated.
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
npm install @prisma/client
|
||||
npx prisma generate --no-engine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error: "Database xxx does not exist"
|
||||
|
||||
**Cause**: DATABASE_URL in `.env` points to non-existent database.
|
||||
|
||||
**Solution**:
|
||||
1. Create database: `CREATE DATABASE xxx;`
|
||||
2. Verify DATABASE_URL in `.env`
|
||||
|
||||
---
|
||||
|
||||
### Error: "No such module 'node:*'"
|
||||
|
||||
**Cause**: `nodejs_compat` flag not enabled.
|
||||
|
||||
**Solution**: Add to `wrangler.jsonc`:
|
||||
```jsonc
|
||||
{
|
||||
"compatibility_flags": ["nodejs_compat"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Package Scripts
|
||||
|
||||
Add to `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"db:generate": "prisma generate --no-engine",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:deploy": "prisma migrate deploy",
|
||||
"db:studio": "prisma studio",
|
||||
"db:push": "prisma db push"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
```bash
|
||||
npm run db:generate # Generate Prisma Client
|
||||
npm run db:migrate # Create and apply migration
|
||||
npm run db:studio # Open Prisma Studio
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Prisma Documentation](https://www.prisma.io/docs)
|
||||
- [Prisma Driver Adapters](https://www.prisma.io/docs/orm/overview/databases/database-drivers)
|
||||
- [Hyperdrive Prisma Example](https://developers.cloudflare.com/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/prisma-orm/)
|
||||
- [Prisma with Workers](https://www.prisma.io/docs/orm/more/under-the-hood/engines#using-custom-engine-binaries)
|
||||
Reference in New Issue
Block a user