diff --git a/.github/tests/orm/hono/run.sh b/.github/tests/orm/hono/run.sh new file mode 100644 index 000000000000..7d10e7676f8d --- /dev/null +++ b/.github/tests/orm/hono/run.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +set -eu + +echo "🔍 Starting test setup for hono..." + +echo "📂 Current working directory before REPO_ROOT: $(pwd)" +echo "📁 Listing contents:" +ls -la + +REPO_ROOT="$(git rev-parse --show-toplevel)" +echo "📌 Detected repo root: $REPO_ROOT" + +cd "$REPO_ROOT/orm/hono" +echo "📂 Changed directory to: $(pwd)" + +echo "📦 Installing test deps..." +npm install + +# Go to Node script dir and install its deps +NODE_SCRIPT_DIR="../../.github/get-ppg-dev" +pushd "$NODE_SCRIPT_DIR" > /dev/null +npm install + +# Start Prisma Dev server +LOG_FILE="./ppg-dev-url.log" +rm -f "$LOG_FILE" +touch "$LOG_FILE" + +echo "🚀 Starting Prisma Dev in background..." +node index.js >"$LOG_FILE" & +NODE_PID=$! + +# Wait for DATABASE_URL +echo "🔎 Waiting for Prisma Dev to emit DATABASE_URL..." +MAX_WAIT=60 +WAITED=0 +until grep -q '^prisma+postgres://' "$LOG_FILE"; do + sleep 1 + WAITED=$((WAITED + 1)) + if [ "$WAITED" -ge "$MAX_WAIT" ]; then + echo "❌ Timeout waiting for DATABASE_URL" + cat "$LOG_FILE" + kill "$NODE_PID" || true + exit 1 + fi +done + +DB_URL=$(grep '^prisma+postgres://' "$LOG_FILE" | tail -1) +export DATABASE_URL="$DB_URL" +echo "✅ DATABASE_URL: $DATABASE_URL" + +popd > /dev/null # Back to orm/hono + +# Run migrations and seed +npx prisma migrate dev --name init +npx prisma db seed + +# Start the app +echo "🚀 Starting Hono app..." +npm run dev & +pid=$! + +sleep 15 + +# Check frontend +echo "🔎 Verifying root frontend route..." +curl --fail 'http://localhost:3000/' + +# Cleanup +echo "🛑 Shutting down Hono app (PID $pid) and Prisma Dev (PID $NODE_PID)..." +kill "$pid" +kill "$NODE_PID" +wait "$NODE_PID" || true diff --git a/orm/hono/.env.example b/orm/hono/.env.example new file mode 100644 index 000000000000..c8baf14fe2e1 --- /dev/null +++ b/orm/hono/.env.example @@ -0,0 +1,2 @@ +# Prisma +DATABASE_URL="" diff --git a/orm/hono/README.md b/orm/hono/README.md new file mode 100644 index 000000000000..ea2dabb4f40d --- /dev/null +++ b/orm/hono/README.md @@ -0,0 +1,146 @@ +# REST API Example with Hono & Prisma Postgres + +This example shows how to implement a **REST API with TypeScript** using [Hono](https://hono.dev/) and [Prisma Client](https://www.prisma.io/docs/concepts/components/prisma-client). It uses a [Prisma Postgres](https://www.prisma.io/postgres) database. + +## Getting started + +### 1. Download example and navigate into the project directory + +Download this example: + +``` +npx try-prisma@latest --template orm/hono --install npm --name hono +``` + +Then, navigate into the project directory: + +``` +cd hono +``` + +
Alternative: Clone the entire repo + +Clone this repository: + +``` +git clone git@github.com:prisma/prisma-examples.git --depth=1 +``` + +Install npm dependencies: + +``` +cd prisma-examples/orm/hono +npm install +``` + +
+ +### 2. Create and seed the database + +Create a new [Prisma Postgres](https://www.prisma.io/docs/postgres/overview) database by executing: + +```terminal +npx prisma init --db +``` + +If you don't have a [Prisma Data Platform](https://console.prisma.io/) account yet, or if you are not logged in, the command will prompt you to log in using one of the available authentication providers. A browser window will open so you can log in or create an account. Return to the CLI after you have completed this step. + +Once logged in (or if you were already logged in), the CLI will prompt you to: +1. Select a **region** (e.g. `us-west-1`) +1. Enter a **project name** + +After successful creation, you will see output similar to the following: + +
+ +CLI output + +```terminal +Let's set up your Prisma Postgres database! +? Select your region: ap-northeast-1 - Asia Pacific (Tokyo) +? Enter a project name: testing-migration +✔ Success! Your Prisma Postgres database is ready ✅ + +We found an existing schema.prisma file in your current project directory. + +--- Database URL --- + +Connect Prisma ORM to your Prisma Postgres database with this URL: + +prisma+postgres://accelerate.prisma-data.net/?api_key=... + +--- Next steps --- + +Go to https://pris.ly/ppg-init for detailed instructions. + +1. Install and use the Prisma Accelerate extension +Prisma Postgres requires the Prisma Accelerate extension for querying. If you haven't already installed it, install it in your project: +npm install @prisma/extension-accelerate + +...and add it to your Prisma Client instance: +import { withAccelerate } from "@prisma/extension-accelerate" + +const prisma = new PrismaClient().$extends(withAccelerate()) + +2. Apply migrations +Run the following command to create and apply a migration: +npx prisma migrate dev + +3. Manage your data +View and edit your data locally by running this command: +npx prisma studio + +...or online in Console: +https://console.prisma.io/{workspaceId}/{projectId}/studio + +4. Send queries from your app +If you already have an existing app with Prisma ORM, you can now run it and it will send queries against your newly created Prisma Postgres instance. + +5. Learn more +For more info, visit the Prisma Postgres docs: https://pris.ly/ppg-docs +``` + +
+ +Locate and copy the database URL provided in the CLI output. Then, create a `.env` file in the project root: + +```bash +touch .env +``` + +Now, paste the URL into it as a value for the `DATABASE_URL` environment variable. For example: + +```bash +# .env +DATABASE_URL=prisma+postgres://accelerate.prisma-data.net/?api_key=ey... +``` + +Run the following command to create tables in your database. This creates the `User` and `Post` tables that are defined in [`prisma/schema.prisma`](./prisma/schema.prisma): + +```terminal +npx prisma migrate dev --name init +``` + +Execute the seed file in [`prisma/seed.ts`](./prisma/seed.ts) to populate your database with some sample data, by running: + +```terminal +npx prisma db seed +``` + +### 3. Start the REST API server + +``` +npm run dev +``` + +The server is now running on `http://localhost:3000`. You can send now the API requests, e.g. [`http://localhost:3000/users`](http://localhost:3000/users). + +## Switch to another database + +If you want to try this example with another database rather than Prisma Postgres, refer to the [Databases](https://www.prisma.io/docs/orm/overview/databases) section in our documentation + +## Next steps + +- Check out the [Prisma docs](https://www.prisma.io/docs) +- Share your feedback on the [Prisma Discord](https://pris.ly/discord/) +- Create issues and ask questions on [GitHub](https://github.com/prisma/prisma/) diff --git a/orm/hono/package.json b/orm/hono/package.json new file mode 100644 index 000000000000..3a43d617b73e --- /dev/null +++ b/orm/hono/package.json @@ -0,0 +1,25 @@ +{ + "name": "my-app", + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" + }, + "dependencies": { + "@hono/node-server": "^1.19.5", + "@prisma/client": "^6.16.3", + "@prisma/extension-accelerate": "^2.0.2", + "dotenv": "^17.2.3", + "hono": "^4.9.9" + }, + "devDependencies": { + "@types/node": "^20.11.17", + "prisma": "^6.16.3", + "tsx": "^4.20.6", + "typescript": "^5.8.3" + } +} diff --git a/orm/hono/prisma/schema.prisma b/orm/hono/prisma/schema.prisma new file mode 100644 index 000000000000..90f567f0b408 --- /dev/null +++ b/orm/hono/prisma/schema.prisma @@ -0,0 +1,35 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init + +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + content String? + published Boolean @default(false) + viewCount Int @default(0) + author User? @relation(fields: [authorId], references: [id]) + authorId Int? +} diff --git a/orm/hono/prisma/seed.ts b/orm/hono/prisma/seed.ts new file mode 100644 index 000000000000..6498421d720c --- /dev/null +++ b/orm/hono/prisma/seed.ts @@ -0,0 +1,57 @@ +import { PrismaClient, Prisma } from "../src/generated/prisma/client.js"; + +const prisma = new PrismaClient(); + +const userData: Prisma.UserCreateInput[] = [ + { + name: 'Alice', + email: 'alice@prisma.io', + posts: { + create: [ + { + title: 'Join the Prisma Discord', + content: 'https://pris.ly/discord', + published: true, + }, + ], + }, + }, + { + name: 'Nilu', + email: 'nilu@prisma.io', + posts: { + create: [ + { + title: 'Follow Prisma on Twitter', + content: 'https://www.twitter.com/prisma', + published: true, + }, + ], + }, + }, + { + name: 'Mahmoud', + email: 'mahmoud@prisma.io', + posts: { + create: [ + { + title: 'Ask a question about Prisma on GitHub', + content: 'https://www.github.com/prisma/prisma/discussions', + published: true, + }, + { + title: 'Prisma on YouTube', + content: 'https://pris.ly/youtube', + }, + ], + }, + }, +] + +export async function main() { + for (const u of userData) { + await prisma.user.create({ data: u }); + } +} + +main(); diff --git a/orm/hono/src/index.ts b/orm/hono/src/index.ts new file mode 100644 index 000000000000..a0d2aa2de544 --- /dev/null +++ b/orm/hono/src/index.ts @@ -0,0 +1,191 @@ +import { Hono } from 'hono'; +import { serve } from '@hono/node-server'; + +import type { Prisma, PrismaClient } from './generated/prisma/client.js'; + +type ContextWithPrisma = { + Variables: { + prisma: PrismaClient; + }; +}; + +const app = new Hono(); + +import withPrisma from './lib/prisma.js'; +import type { PostCreateInput } from './generated/prisma/models.js'; + +app.post('/signup', withPrisma, async (c) => { + const prisma = c.get('prisma'); + const { name, email, posts } = await c.req.json<{ + name: string; + email: string; + posts: Array; + }>(); + + const postData = posts + ? posts.map((post: Prisma.PostCreateInput) => { + return { title: post.title, content: post.content || undefined }; + }) + : []; + + const newUser = await prisma.user.create({ + data: { + name, + email, + posts: { + create: postData, + }, + }, + }); + + return c.json(newUser, 201); +}); + +app.post('/post', withPrisma, async (c) => { + const prisma = c.get('prisma'); + const { + title, + content, + authorEmail: email, + } = await c.req.json<{ + title: string; + content: string; + authorEmail: string; + }>(); + const newPost = await prisma.post.create({ + data: { + title, + content, + author: { connect: { email } }, + }, + }); + return c.json(newPost, 201); +}); + +app.put('/post/:id/views', withPrisma, async (c) => { + const prisma = c.get('prisma'); + const id = Number(c.req.param('id')); + + try { + const post = await prisma.post.update({ + where: { id }, + data: { viewCount: { increment: 1 } }, + }); + return c.json(post, 201); + } catch { + return c.json( + { + error: `Post with ID ${id} does not exist in the database`, + }, + 404, + ); + } +}); + +app.put('/publish/:id', withPrisma, async (c) => { + const prisma = c.get('prisma'); + const id = Number(c.req.param('id')); + const postToUpdate = await prisma.post.findUnique({ + where: { id }, + }); + + if (!postToUpdate) { + c.status(404); + return c.json({ + error: `Post with ID ${id} does not exist in the database`, + }); + } + + const updatedPost = await prisma.post.update({ + where: { + id, + }, + data: { + published: !postToUpdate.published, + }, + }); + return c.json(updatedPost, 201); +}); + +app.delete('/post/:id', withPrisma, async (c) => { + const prisma = c.get('prisma'); + const id = Number(c.req.param('id')); + + try { + const deletedPost = await prisma.post.delete({ + where: { + id, + }, + }); + + return c.json(deletedPost); + } catch { + return c.json( + { + error: `Post with ID ${id} does not exist in the database`, + }, + 404, + ); + } +}); + +app.get('/users', withPrisma, async (c) => { + const prisma = c.get('prisma'); + const users = await prisma.user.findMany({ + include: { posts: true }, + }); + return c.json(users); +}); + +app.get('/users/:id/drafts', withPrisma, async (c) => { + const prisma = c.get('prisma'); + const id = Number(c.req.param('id')); + const drafts = await prisma.post.findMany({ + where: { + authorId: id, + published: false, + }, + }); + return c.json(drafts); +}); + +app.get('/post/:id', withPrisma, async (c) => { + const prisma = c.get('prisma'); + const id = Number(c.req.param('id')); + const post = await prisma.post.findUnique({ + where: { + id, + }, + }); + return c.json(post); +}); + +app.get('/feed', withPrisma, async (c) => { + const prisma = c.get('prisma'); + const { searchString, skip, take, orderBy } = c.req.query(); + const or = searchString + ? { + OR: [ + { title: { contains: searchString as string } }, + { content: { contains: searchString as string } }, + ], + } + : {}; + + const posts = await prisma.post.findMany({ + where: { published: true, ...or }, + include: { author: true }, + take: Number(take) || undefined, + skip: Number(skip) || undefined, + orderBy: orderBy ? [{ id: orderBy as Prisma.SortOrder }] : undefined, + }); + return c.json(posts); +}); + +app.get('/', (c) => { + return c.text('Hello Hono!'); +}); + +serve({ fetch: app.fetch, port: 3000 }, (info) => { + console.log(`Server is running on http://localhost:${info.port}`); +}); diff --git a/orm/hono/src/lib/prisma.ts b/orm/hono/src/lib/prisma.ts new file mode 100644 index 000000000000..db1ae183dff2 --- /dev/null +++ b/orm/hono/src/lib/prisma.ts @@ -0,0 +1,22 @@ +import type { Context, Next } from 'hono' +import { PrismaClient } from '../generated/prisma/client.js' +import { withAccelerate } from '@prisma/extension-accelerate' + +import * as dotenv from 'dotenv' +dotenv.config() + +const databaseUrl = process.env.DATABASE_URL +function withPrisma(c: Context, next: Next) { + if (!c.get('prisma')) { + if (!databaseUrl) { + throw new Error('DATABASE_URL is not set') + } + const prisma = new PrismaClient({ datasourceUrl: databaseUrl }).$extends( + withAccelerate(), + ) + + c.set('prisma', prisma) + } + return next() +} +export default withPrisma diff --git a/orm/hono/tsconfig.json b/orm/hono/tsconfig.json new file mode 100644 index 000000000000..b55223e0d5cc --- /dev/null +++ b/orm/hono/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "strict": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "types": [ + "node" + ], + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx", + "outDir": "./dist" + }, + "exclude": ["node_modules"] +}