diff --git a/README.md b/README.md index bf3d02b7595c..ab86020af845 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ The [`pulse`](./pulse) folder contains examples of projects using [Prisma Pulse] | ---------------------------------------------------------------------- | ------------------------------------------------------------------------ | | [`starter`](./pulse/starter) | A Prisma Pulse starter app | | [`email-with-resend`](./pulse/email-with-resend) | An example app to send emails to new users using Prisma Pulse and Resend | +| [`fullstack-leaderboard`](./pulse/fullstack-leaderboard) | An example app to send emails to new users using Prisma Pulse and Resend | +| [`fullstack-simple-chat`](./pulse/fullstack-simple-chat) | An example app to send emails to new users using Prisma Pulse and Resend | ## Prisma ORM (TypeScript) diff --git a/pulse/email-with-resend/README.md b/pulse/email-with-resend/README.md index 0e05454ac718..0590dd0b0632 100644 --- a/pulse/email-with-resend/README.md +++ b/pulse/email-with-resend/README.md @@ -1,7 +1,10 @@ # Prisma Pulse Example: Send Onboarding Email with Resend -This repository contains an example app that uses Prisma Pulse to detect when new users are added to the database and sends them an onboarding email using [Resend](https://resend.com/). +This repository contains an example app that uses Prisma Pulse to detect when new users are added to the database and sends them an onboarding email: +- [Prisma Pulse](https://www.prisma.io/data-platform/pulse) to get real-time updates from the database +- [Resend](https://resend.com/) to send emails +- [PostgreSQL](https://www.postgresql.org/) as the database ## Prerequisites @@ -9,7 +12,7 @@ To successfully run the project, you will need the following: - The **connection string** of a Pulse-ready database (if you don't have one yet, you can configure your database following the instructions in our [docs](https://www.prisma.io/docs/pulse/database-setup) or [use a Railway template](https://railway.app/template/pulse-pg?referralCode=VQ09uv)) - A **Pulse API key** which you can get by enabling Pulse in a project in your [Prisma Data Platform](https://pris.ly/pdp) account (learn more in the [docs](https://www.prisma.io/docs/platform/concepts/environments#api-keys)) -- A **Resend API Key** which you can get from your [Resend account](https://resend.com/api-keys) +- A **Resend API Key** which you can get from your [Resend account](https://resend.com/api-keys) (note that if you're on the [free plan](https://resend.com/blog/new-free-tier), you'll only be able to send emails to exactly _one_ email address per day) ## Getting started @@ -54,7 +57,7 @@ You now have an empty `User` table in your database. ### 4. Start the Pulse subscription -Run the [script](./index.ts) that contains the code to subscribe to database events: +Run the [script](./index.ts) that contains the code to stream database events: ``` npm run dev @@ -71,6 +74,8 @@ The following instructions use [Prisma Studio](https://www.prisma.io/studio) to 3. You will receive a new email in the inbox of the specified `email`. For example: When a new user with an email of `datta@prisma.io` and a name of `Ankur` is created, the email inbox for `datta@prisma.io` should have received the following email (the copy for this email is hardcoded in [`./script.ts`](./script.ts#L33)): ![image.png](./images/email.png) +> **Note**: If you can see the event logged to the terminal but didn't receive an email, it may be that you have maxed out the receiver mail addresses on the [free tier](https://resend.com/blog/new-free-tier) for the day. + ## Resources - [Pulse examples](https://pris.ly/pulse-examples) diff --git a/pulse/email-with-resend/package.json b/pulse/email-with-resend/package.json index 8eadb2f0a13e..cbdd28551908 100644 --- a/pulse/email-with-resend/package.json +++ b/pulse/email-with-resend/package.json @@ -6,7 +6,7 @@ }, "dependencies": { "@prisma/client": "5.14.0", - "@prisma/extension-pulse": "1.0.2", + "@prisma/extension-pulse": "dev", "@types/node": "20.12.12", "dotenv": "16.4.5", "resend": "3.2.0" @@ -16,4 +16,4 @@ "ts-node": "10.9.2", "typescript": "5.4.5" } -} \ No newline at end of file +} diff --git a/pulse/email-with-resend/script.ts b/pulse/email-with-resend/script.ts index 2d9c2d8e311c..acdbc36642d9 100644 --- a/pulse/email-with-resend/script.ts +++ b/pulse/email-with-resend/script.ts @@ -42,22 +42,20 @@ const sendUserCreationEmail = async ({ email, name }: UserEmail) => { return await resendClient.emails.send(emailOptions); }; -// Subscribe to user creation events and send emails -const emailSubscriber = async () => { - const subscription = await prisma.user.subscribe({ - create: {}, +// Stream user creation events and send emails +const emailStream = async () => { + const stream = await prisma.user.stream({ + name: 'all-created-users', // Add `name` so that we never lose events + create: {} }); process.on("exit", (code) => { - console.log("Closing Prisma Pulse Subscription."); - subscription.stop(); + console.log("Closing Prisma Pulse Stream."); + stream.stop(); }); - if (subscription instanceof Error) { - throw subscription; - } - for await (const event of subscription) { + for await (const event of stream) { console.log("Received event:", event); const { email, name } = event.created; @@ -72,7 +70,7 @@ const emailSubscriber = async () => { // Main function async function main() { - await emailSubscriber(); + await emailStream(); } // Run the main function diff --git a/pulse/fullstack-leaderboard/.eslintrc.json b/pulse/fullstack-leaderboard/.eslintrc.json new file mode 100644 index 000000000000..bffb357a7122 --- /dev/null +++ b/pulse/fullstack-leaderboard/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/pulse/fullstack-leaderboard/.gitignore b/pulse/fullstack-leaderboard/.gitignore new file mode 100644 index 000000000000..1d94587008ac --- /dev/null +++ b/pulse/fullstack-leaderboard/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts +dist diff --git a/pulse/fullstack-leaderboard/README.md b/pulse/fullstack-leaderboard/README.md new file mode 100644 index 000000000000..ac48a474b8a1 --- /dev/null +++ b/pulse/fullstack-leaderboard/README.md @@ -0,0 +1,118 @@ +# Prisma Pulse Example: Fullstack Leaderboard (Next.js) + +![](./leaderboard.gif) + +This repository contains an example app that uses Prisma Pulse in a fullstack application to display and update a real-time leaderboard: + +- [Next.js](https://nextjs.org/) (_frontend_) with a [custom server](https://nextjs.org/docs/pages/building-your-application/configuring/custom-server) (_backend_) +- [React Flip Move](https://github.com/joshwcomeau/react-flip-move) for animating React components +- [socket.io](https://socket.io/) for the websocket connection between client and server +- [Prisma Pulse](https://www.prisma.io/data-platform/pulse) to get real-time updates from the database +- [PostgreSQL](https://www.postgresql.org/) as the database + +> **Note**: The custom server is required because Pulse requires a long-running connection to the database. As an alternative to the custom server included in this app, you can also build your own server using a library/framework like Express, Fastify or NestJS. + + +## Prerequisites + +To successfully run the project, you will need the following: + +- The **connection string** of a Pulse-ready database (if you don't have one yet, you can configure your database following the instructions in our [docs](https://www.prisma.io/docs/pulse/database-setup) or [use a Railway template](https://railway.app/template/pulse-pg?referralCode=VQ09uv)) +- A **Pulse API key** which you can get by enabling Pulse in a project in your [Prisma Data Platform](https://pris.ly/pdp) account (learn more in the [docs](https://www.prisma.io/docs/platform/concepts/environments#api-keys)) + +## Getting started + +### 1. Clone the respository + +Clone the repository, navigate into it and install dependencies: + +``` +git clone git@github.com:prisma/prisma-examples.git --depth=1 +cd prisma-examples/pulse/fullstack-leaderboard +npm install +``` + +### 2. Configure environment variables + +Create a `.env` in the root of the project directory: + +```bash +touch .env +``` + +Now, open the `.env` file and update the `DATABASE_URL` and `PULSE_API_KEY` environment variables with the values of your connection string and your Pulse API keys: + +```bash +# .env +DATABASE_URL="__YOUR_DATABASE_CONNECTION_STRING__" +PULSE_API_KEY="__YOUR_PULSE_API_KEY__" +``` + +Note that `__YOUR_DATABASE_CONNECTION_STRING__` and `__YOUR_PULSE_API_KEY__` are **placeholder values that you must replace** with the values of your own connection string and Pulse API key. + +### 3. Run a database migration to create the `Player` table + +The [Prisma schema file](./prisma/schema.prisma) in this project contains a single `Player` model. You can map this model to the database and create the corresponding `Player` table using the following command: + +``` +npx prisma migrate dev --name init +``` + +You now have a table called `Player` in your database. + +Next, run the following command to [seed](./prisma/seed.ts) the database with some dummy data: + +``` +npx prisma db seed +``` + +The is invoked the [seed script](./prisma/seed.ts) and created three `Player` records in the database. + + +### 4. Start the server + +Make sure you're inside the [`server`](./server) directory and start the long-running server that streams changes from the database: + +``` +npm run server +``` + +The server will accept WebSocket connections at `http://localhost:3001`. + +Next, run the Next.js app: + +``` +npm run dev +``` + +You can open the app at [`http://localhost:3000`](http://localhost:3000). + +Every new tab/window you open in your browser and point to that URL will instantiate its own WebSocket connection to the long-running server. + +### 5. Use the app + +Click on the buttons at the bottom to increase the score of a player and see how the leaderboard updates in real-time. + +## Deployment + +Because the app requires a [custom server](https://nextjs.org/docs/pages/building-your-application/configuring/custom-server) to enable the WebSocket connections, you need to deploy the frontend and the [backend](./src/server.ts) separately. + +### Deploying on Railway + +In the following, you find instructions to deploy the app via [Railway](https://railway.app). In order to deploy successfully, you need: +- a Railway account +- the Railway CLI installed on your machine + + +#### Deploying the frontend + +Create a new + +#### Deploying the backend + +## Resources + +- [Pulse examples](https://pris.ly/pulse-examples) +- [Pulse documentation](https://pris.ly/pulse-docs) +- [Pulse announcement blog post](https://pris.ly/gh/pulse-ga) +- [Prisma Discord](https://pris.ly/discord) diff --git a/pulse/fullstack-leaderboard/leaderboard.gif b/pulse/fullstack-leaderboard/leaderboard.gif new file mode 100644 index 000000000000..8a4cbc062381 Binary files /dev/null and b/pulse/fullstack-leaderboard/leaderboard.gif differ diff --git a/pulse/fullstack-leaderboard/leaderboard.png b/pulse/fullstack-leaderboard/leaderboard.png new file mode 100644 index 000000000000..4ee289879224 Binary files /dev/null and b/pulse/fullstack-leaderboard/leaderboard.png differ diff --git a/pulse/fullstack-leaderboard/next.config.mjs b/pulse/fullstack-leaderboard/next.config.mjs new file mode 100644 index 000000000000..4678774e6d60 --- /dev/null +++ b/pulse/fullstack-leaderboard/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/pulse/fullstack-leaderboard/package.json b/pulse/fullstack-leaderboard/package.json new file mode 100644 index 000000000000..6fe7f5cc0d2e --- /dev/null +++ b/pulse/fullstack-leaderboard/package.json @@ -0,0 +1,38 @@ +{ + "name": "pulse-leaderboard", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "server": "ts-node --project tsconfig.server.json src/server.ts", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@prisma/client": "^5.14.0", + "@prisma/extension-pulse": "^1.1.0", + "@types/socket.io": "^3.0.2", + "next": "14.2.3", + "react": "^18", + "react-dom": "^18", + "react-flip-move": "^3.0.5", + "socket.io": "^4.7.5", + "socket.io-client": "^4.7.5" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.3", + "postcss": "^8", + "prisma": "^5.14.0", + "tailwindcss": "^3.4.1", + "ts-node": "^10.9.2", + "typescript": "^5" + }, + "prisma": { + "seed": "ts-node --project tsconfig.server.json prisma/seed.ts" + } +} diff --git a/pulse/fullstack-leaderboard/postcss.config.mjs b/pulse/fullstack-leaderboard/postcss.config.mjs new file mode 100644 index 000000000000..1a69fd2a450a --- /dev/null +++ b/pulse/fullstack-leaderboard/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/pulse/fullstack-leaderboard/prisma/schema.prisma b/pulse/fullstack-leaderboard/prisma/schema.prisma new file mode 100644 index 000000000000..d61d0d3b9683 --- /dev/null +++ b/pulse/fullstack-leaderboard/prisma/schema.prisma @@ -0,0 +1,20 @@ +// 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-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Player { + id Int @id @default(autoincrement()) + username String @unique + points Int @default(0) +} diff --git a/pulse/fullstack-leaderboard/prisma/seed.ts b/pulse/fullstack-leaderboard/prisma/seed.ts new file mode 100644 index 000000000000..6c654ee67d35 --- /dev/null +++ b/pulse/fullstack-leaderboard/prisma/seed.ts @@ -0,0 +1,22 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +async function main() { + const players = await prisma.player.createMany({ + data: [ + { + username: "Marc", + }, + { + username: "Ankur", + }, + { + username: "Jon", + }, + ], + }); + console.log(`Seeded the database with ${players.count} players.`); +} + +main(); diff --git a/pulse/fullstack-leaderboard/public/next.svg b/pulse/fullstack-leaderboard/public/next.svg new file mode 100644 index 000000000000..5174b28c565c --- /dev/null +++ b/pulse/fullstack-leaderboard/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pulse/fullstack-leaderboard/public/vercel.svg b/pulse/fullstack-leaderboard/public/vercel.svg new file mode 100644 index 000000000000..d2f84222734f --- /dev/null +++ b/pulse/fullstack-leaderboard/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pulse/fullstack-leaderboard/src/app/actions.ts b/pulse/fullstack-leaderboard/src/app/actions.ts new file mode 100644 index 000000000000..9ecb8cef27ec --- /dev/null +++ b/pulse/fullstack-leaderboard/src/app/actions.ts @@ -0,0 +1,11 @@ +"use server"; +import prisma from "@/lib/prisma"; + +export async function addPoints({ points, playerId }: { points: number; playerId: number }) { + console.log(`addPoints, `, points, playerId); + const updatedPlayer = await prisma.player.update({ + where: { id: playerId }, + data: { points: { increment: points } }, + }); + console.log(`Player ${updatedPlayer.username} now has ${updatedPlayer.points}.`); +} diff --git a/pulse/fullstack-leaderboard/src/app/favicon.ico b/pulse/fullstack-leaderboard/src/app/favicon.ico new file mode 100644 index 000000000000..718d6fea4835 Binary files /dev/null and b/pulse/fullstack-leaderboard/src/app/favicon.ico differ diff --git a/pulse/fullstack-leaderboard/src/app/globals.css b/pulse/fullstack-leaderboard/src/app/globals.css new file mode 100644 index 000000000000..af1af3ed1116 --- /dev/null +++ b/pulse/fullstack-leaderboard/src/app/globals.css @@ -0,0 +1,46 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} + +@keyframes blink { + 0%, 100% { + background-color: white; + } + 50% { + background-color: #16A394; /*16A394*/ + } +} + +.blink { + animation: blink 1s ease-out; +} diff --git a/pulse/fullstack-leaderboard/src/app/layout.tsx b/pulse/fullstack-leaderboard/src/app/layout.tsx new file mode 100644 index 000000000000..3314e4780a0c --- /dev/null +++ b/pulse/fullstack-leaderboard/src/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/pulse/fullstack-leaderboard/src/app/page.tsx b/pulse/fullstack-leaderboard/src/app/page.tsx new file mode 100644 index 000000000000..646553627750 --- /dev/null +++ b/pulse/fullstack-leaderboard/src/app/page.tsx @@ -0,0 +1,34 @@ +"use server" + +import prisma from "@/lib/prisma"; +import UpvoteButton from "@/components/UpvoteButton"; +import Leaderboard from "@/components/Leaderboard"; + +export default async function Home() { + + async function getPlayers() { + console.log(`getPlayers`) + const players = await prisma.player.findMany(); + return players; + } + + const players = await getPlayers(); + console.log(players); + + return ( +
+ +
+ {players.map((player) => { + return ; + })} +
+

+ ⚡️ This leaderboard is updated in real-time when a value changes in the database. Hit one of the buttons above to update the score of a player. +

+

+ 💡 Tip: Open the app in multiple browser windows to see the update in multiple places at once. +

+
+ ); +} \ No newline at end of file diff --git a/pulse/fullstack-leaderboard/src/components/Leaderboard.tsx b/pulse/fullstack-leaderboard/src/components/Leaderboard.tsx new file mode 100644 index 000000000000..35729b3c0504 --- /dev/null +++ b/pulse/fullstack-leaderboard/src/components/Leaderboard.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { Player } from "@prisma/client"; +import { io, Socket } from "socket.io-client"; +import { useState, useEffect, MutableRefObject, useRef } from "react"; +import FlipMove from "react-flip-move"; + +interface UpdateEventType { + after: Player; +} + +export default function Leaderboard({ initialPlayers }: { initialPlayers: Player[] }) { + const [players, setPlayers] = useState(initialPlayers || []); + const [updatedPlayerId, setUpdatedPlayerId] = useState(null); + + let socketRef: MutableRefObject = useRef(null); + + useEffect(() => { + const updatePoints = (updatedPlayer: Player) => { + setPlayers((prevPlayers) => + prevPlayers.map((player) => + player.id === updatedPlayer.id ? updatedPlayer : player + ) + ); + setUpdatedPlayerId(updatedPlayer.id); + setTimeout(() => setUpdatedPlayerId(null), 1000); // Reset updated player ID after 1 second + }; + + const url = process.env.SERVER_URL ?? `http://localhost:3001`; + socketRef.current = io(url); + + // An update to a player's points + socketRef.current.on("player_points", (event: UpdateEventType) => { + console.log(`received UPDATE event from server`, event); + updatePoints(event.after); + }); + + return () => { + socketRef.current?.off("player_points"); + }; + }, [players]); + + const sortedPlayers = [...players].sort((a, b) => b.points - a.points); + + return ( +
+

+ 🏆 Welcome to the Real-Time Leaderboard 🏆 +

+ + {sortedPlayers.map((player, i) => ( +
+
+ {i === 0 ? `${player.username} 🥇` : player.username} +
+
+ {player.points} +
+
+ ))} +
+
+ ); +} diff --git a/pulse/fullstack-leaderboard/src/components/UpvoteButton.tsx b/pulse/fullstack-leaderboard/src/components/UpvoteButton.tsx new file mode 100644 index 000000000000..c15712ffa210 --- /dev/null +++ b/pulse/fullstack-leaderboard/src/components/UpvoteButton.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { addPoints } from "@/app/actions"; +import { Player } from "@prisma/client"; + +export default function UpvoteButton({ player }: { player: Player }) { + return ( + + ); +} diff --git a/pulse/fullstack-leaderboard/src/lib/prisma.ts b/pulse/fullstack-leaderboard/src/lib/prisma.ts new file mode 100644 index 000000000000..cdc86c799651 --- /dev/null +++ b/pulse/fullstack-leaderboard/src/lib/prisma.ts @@ -0,0 +1,17 @@ +import { PrismaClient } from "@prisma/client"; +import { withPulse } from "@prisma/extension-pulse"; + +const prismaClientSingleton = () => { + // return new PrismaClient() //.$extends(withPulse({ apiKey: process.env.PULSE_API_KEY })); + return new PrismaClient().$extends(withPulse({ apiKey: process.env.PULSE_API_KEY || '' })); +}; + +declare const globalThis: { + prismaGlobal: ReturnType; +} & typeof global; + +const prisma = globalThis.prismaGlobal ?? prismaClientSingleton(); + +export default prisma; + +if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma; diff --git a/pulse/fullstack-leaderboard/src/server.ts b/pulse/fullstack-leaderboard/src/server.ts new file mode 100644 index 000000000000..ec2f5c936098 --- /dev/null +++ b/pulse/fullstack-leaderboard/src/server.ts @@ -0,0 +1,48 @@ +import http from "http"; +import prisma from "./lib/prisma"; +import { Server } from "socket.io"; + +const httpServer = http.createServer((req, res) => { + // Define the routes + if (req.method === "GET" && req.url === "/") { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("Server is running ..."); + } else { + // Handle 404 Not Found + res.writeHead(404, { "Content-Type": "text/plain" }); + res.end("404 Not Found"); + } +}); + +const corsOrigins = [ + process.env.CLIENT_URL ?? "http://localhost:3000", + "http://localhost:3000", +] +console.log(`cors origins: `, corsOrigins) + +const io = new Server(httpServer, { + cors: { + origin: corsOrigins, + methods: ["GET", "POST"], + credentials: true, + }, +}); + +const PORT = process.env.PORT || 3001; +httpServer.listen(PORT, async () => { + console.log(`socket.io server is running on port ${PORT}`); + await streamPlayerUpdates(io); +}); + +async function streamPlayerUpdates(io: Server) { + const stream = await prisma.player.stream(); + + // Handle Prisma stream events + for await (const event of stream) { + console.log(`received event: `, event); + + if (event.action === "update") { + io.sockets.emit("player_points", event); + } + } +} diff --git a/pulse/fullstack-leaderboard/tailwind.config.ts b/pulse/fullstack-leaderboard/tailwind.config.ts new file mode 100644 index 000000000000..dc122aeb1104 --- /dev/null +++ b/pulse/fullstack-leaderboard/tailwind.config.ts @@ -0,0 +1,25 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + backgroundImage: { + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + }, + colors: { + indigo600: '#5A67D8', + indigo800: '#434190', + // You can add more custom colors here + }, + }, + }, + plugins: [], +}; +export default config; diff --git a/pulse/fullstack-leaderboard/tsconfig.json b/pulse/fullstack-leaderboard/tsconfig.json new file mode 100644 index 000000000000..33de81a3731e --- /dev/null +++ b/pulse/fullstack-leaderboard/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "customConditions": ["node"], + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/pulse/fullstack-leaderboard/tsconfig.server.json b/pulse/fullstack-leaderboard/tsconfig.server.json new file mode 100644 index 000000000000..9902cccc4b4c --- /dev/null +++ b/pulse/fullstack-leaderboard/tsconfig.server.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "dist", + "lib": ["es2019"], + "target": "es2019", + "isolatedModules": false, + "noEmit": false + }, + "include": ["server.ts"] +} diff --git a/pulse/fullstack-simple-chat/README.md b/pulse/fullstack-simple-chat/README.md new file mode 100644 index 000000000000..2d2d8e494f56 --- /dev/null +++ b/pulse/fullstack-simple-chat/README.md @@ -0,0 +1,111 @@ +# Prisma Pulse Example: Fullstack Simple Chat (Next.js & Express) + +![](./tbd.gif) + +This repository contains an example app that uses Prisma Pulse in a fullstack chat application: + +- [Next.js](https://nextjs.org/) as the frontend +- [Express](https://expressjs.com) as the backend +- [socket.io](https://socket.io/) for the websocket connection between client and server +- [Prisma Pulse](https://www.prisma.io/data-platform/pulse) to get real-time updates from the database +- [PostgreSQL](https://www.postgresql.org/) as the database + +## Prerequisites + +To successfully run the project, you will need the following: + +- The **connection string** of a Pulse-ready database (if you don't have one yet, you can configure your database following the instructions in our [docs](https://www.prisma.io/docs/pulse/database-setup) or [use a Railway template](https://railway.app/template/pulse-pg?referralCode=VQ09uv)) +- A **Pulse API key** which you can get by enabling Pulse in a project in your [Prisma Data Platform](https://pris.ly/pdp) account (learn more in the [docs](https://www.prisma.io/docs/platform/concepts/environments#api-keys)) + +## Getting started + +### 1. Clone the respository + +Clone the repository, navigate into it and install dependencies in the [`client`](./client) and [`server`](./server) directories: + +``` +git clone git@github.com:prisma/prisma-examples.git --depth=1 +cd prisma-examples/pulse/fullstack-simple-chat/client +npm install +cd ../server +npm install +``` + +### 2. Configure environment variables + +Create a `.env` file in the [`./server`](./server) directory: + +```bash +# make sure you're inside the `server` directory +touch .env +``` + +Now, open the `./server/.env` file and update the `DATABASE_URL` and `PULSE_API_KEY` environment variables with the values of your connection string, your Pulse and Resend API keys: + +```bash +# ./server/.env +DATABASE_URL="__YOUR_DATABASE_CONNECTION_STRING__" +PULSE_API_KEY="__YOUR_PULSE_API_KEY__" +``` + +Note that `__YOUR_DATABASE_CONNECTION_STRING__` and `__YOUR_PULSE_API_KEY__` are **placeholder values that you must replace** with the values of your own connection string and Pulse API key. + +### 3. Run a database migration to create the `Message` table + +The [Prisma schema file](./prisma/schema.prisma) in this project contains a single `Message` model. You can map this model to the database and create the corresponding `Message` table using the following command: + +``` +npx prisma migrate dev --name init +``` + +You now have a table called `Message` in your database. + + +### 4. Start the server + +Make sure you're inside the [`./server`](./server) directory and start the long-running server that streams changes from the database: + +```bash +# make sure you're inside the `server` directory +npm run dev +``` + +The server will accept WebSocket connections at `http://localhost:3001`. + +Next, open a new terminal tab/window to run the Next.js app from inside the [`./client`](./client) directory: + +```bash +# make sure you're inside the `client` directory +npm run dev +``` + +You can open the app at [`http://localhost:3000`](http://localhost:3000). + +Every new tab/window you open in your browser and point to that URL will instantiate its own WebSocket connection to the long-running server. + +### 5. Use the app + +Write text into the chat box and send it. If you open multiple tabs/windows, new users will appear who can contribute to the chat as well. The text will appear in all tabs/windows at the same time. + +## Deployment + + +### Deploying on Railway + +In the following, you find instructions to deploy the app via [Railway](https://railway.app). In order to deploy successfully, you need: +- a Railway account +- the Railway CLI installed on your machine + + +#### Deploying the frontend + +Create a new + +#### Deploying the backend + +## Resources + +- [Pulse examples](https://pris.ly/pulse-examples) +- [Pulse documentation](https://pris.ly/pulse-docs) +- [Pulse announcement blog post](https://pris.ly/gh/pulse-ga) +- [Prisma Discord](https://pris.ly/discord) diff --git a/pulse/fullstack-simple-chat/client/.eslintrc.json b/pulse/fullstack-simple-chat/client/.eslintrc.json new file mode 100644 index 000000000000..bffb357a7122 --- /dev/null +++ b/pulse/fullstack-simple-chat/client/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/pulse/fullstack-simple-chat/client/.gitignore b/pulse/fullstack-simple-chat/client/.gitignore new file mode 100644 index 000000000000..00bba9bb2902 --- /dev/null +++ b/pulse/fullstack-simple-chat/client/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/pulse/fullstack-simple-chat/client/app/favicon.ico b/pulse/fullstack-simple-chat/client/app/favicon.ico new file mode 100644 index 000000000000..718d6fea4835 Binary files /dev/null and b/pulse/fullstack-simple-chat/client/app/favicon.ico differ diff --git a/pulse/fullstack-simple-chat/client/app/globals.css b/pulse/fullstack-simple-chat/client/app/globals.css new file mode 100644 index 000000000000..fd81e885836d --- /dev/null +++ b/pulse/fullstack-simple-chat/client/app/globals.css @@ -0,0 +1,27 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} diff --git a/pulse/fullstack-simple-chat/client/app/layout.tsx b/pulse/fullstack-simple-chat/client/app/layout.tsx new file mode 100644 index 000000000000..40e027fbefc1 --- /dev/null +++ b/pulse/fullstack-simple-chat/client/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import './globals.css' + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'Create Next App', + description: 'Generated by create next app', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/pulse/fullstack-simple-chat/client/app/page.tsx b/pulse/fullstack-simple-chat/client/app/page.tsx new file mode 100644 index 000000000000..a1bba65a18ba --- /dev/null +++ b/pulse/fullstack-simple-chat/client/app/page.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useState, useEffect, useRef, MutableRefObject } from "react"; +import { io, Socket } from "socket.io-client"; + +interface Message { + text: string; + createdAt: string; + senderSocketId: string; +} + +export default function Home() { + const [message, setMessage] = useState(""); + const [messageHistory, setMessageHistory] = useState([]); + + let socketRef: MutableRefObject = useRef(null); + + const newMessageReceived = (message: Message) => { + console.log(`Received message: `, message); + setMessageHistory((oldMessageHistory) => [...oldMessageHistory, message]); + }; + + useEffect(() => { + const url = `http://localhost:4000`; + + async function fetchMessageHistory() { + const responseData = await fetch(`${url}/messages`); + const response = await responseData.json(); + setMessageHistory(response); + } + fetchMessageHistory(); + + socketRef.current = io(url); + socketRef.current.on("chat-message", newMessageReceived); + + return () => { + socketRef.current?.off("chat-message", newMessageReceived); + }; + }, []); + + const sendMessage = async (e: any) => { + e.preventDefault(); + const newMessage = message; + setMessage(``); + console.log(`Send message`, newMessage); + socketRef.current?.emit(`chat-message`, newMessage); + }; + + return ( +
+
+ {messageHistory.map((message, i) => { + return
{message.senderSocketId} ({message.createdAt}): {message.text}
; + })} +
+ +
+
+ setMessage(e.target.value)} + id="message" + className="focus:outline-none px-2 flex-1 rounded-xl" + type="text" + placeholder="What do you want to say?" + /> + +
+
+
+ ); +} diff --git a/pulse/fullstack-simple-chat/client/next.config.js b/pulse/fullstack-simple-chat/client/next.config.js new file mode 100644 index 000000000000..53e2dc06756d --- /dev/null +++ b/pulse/fullstack-simple-chat/client/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: false +} + +module.exports = nextConfig diff --git a/pulse/fullstack-simple-chat/client/package.json b/pulse/fullstack-simple-chat/client/package.json new file mode 100644 index 000000000000..1938786aa170 --- /dev/null +++ b/pulse/fullstack-simple-chat/client/package.json @@ -0,0 +1,28 @@ +{ + "name": "client", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "14.0.0", + "react": "^18", + "react-dom": "^18", + "socket.io-client": "^4.7.2" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "autoprefixer": "^10", + "eslint": "^8", + "eslint-config-next": "14.0.0", + "postcss": "^8", + "tailwindcss": "^3", + "typescript": "^5" + } +} diff --git a/pulse/fullstack-simple-chat/client/postcss.config.js b/pulse/fullstack-simple-chat/client/postcss.config.js new file mode 100644 index 000000000000..33ad091d26d8 --- /dev/null +++ b/pulse/fullstack-simple-chat/client/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/pulse/fullstack-simple-chat/client/public/next.svg b/pulse/fullstack-simple-chat/client/public/next.svg new file mode 100644 index 000000000000..5174b28c565c --- /dev/null +++ b/pulse/fullstack-simple-chat/client/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pulse/fullstack-simple-chat/client/public/vercel.svg b/pulse/fullstack-simple-chat/client/public/vercel.svg new file mode 100644 index 000000000000..d2f84222734f --- /dev/null +++ b/pulse/fullstack-simple-chat/client/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pulse/fullstack-simple-chat/client/tailwind.config.ts b/pulse/fullstack-simple-chat/client/tailwind.config.ts new file mode 100644 index 000000000000..c7ead804652e --- /dev/null +++ b/pulse/fullstack-simple-chat/client/tailwind.config.ts @@ -0,0 +1,20 @@ +import type { Config } from 'tailwindcss' + +const config: Config = { + content: [ + './pages/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + './app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + backgroundImage: { + 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', + 'gradient-conic': + 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', + }, + }, + }, + plugins: [], +} +export default config diff --git a/pulse/fullstack-simple-chat/client/tsconfig.json b/pulse/fullstack-simple-chat/client/tsconfig.json new file mode 100644 index 000000000000..3b412f2f55d9 --- /dev/null +++ b/pulse/fullstack-simple-chat/client/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "noImplicitAny": false, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/pulse/fullstack-simple-chat/server/.gitignore b/pulse/fullstack-simple-chat/server/.gitignore new file mode 100644 index 000000000000..11ddd8dbe662 --- /dev/null +++ b/pulse/fullstack-simple-chat/server/.gitignore @@ -0,0 +1,3 @@ +node_modules +# Keep environment variables out of version control +.env diff --git a/pulse/fullstack-simple-chat/server/package.json b/pulse/fullstack-simple-chat/server/package.json new file mode 100644 index 000000000000..7328fc714c85 --- /dev/null +++ b/pulse/fullstack-simple-chat/server/package.json @@ -0,0 +1,26 @@ +{ + "version": "1.0.0", + "description": "", + "main": "index.ts", + "scripts": { + "dev": "ts-node src/index.ts", + "build": "npx tsc" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.5.2", + "@prisma/extension-pulse": "^1.1.0", + "cors": "^2.8.5", + "express": "^4.18.2", + "socket.io": "^4.7.2" + }, + "devDependencies": { + "@types/cors": "^2.8.13", + "@types/express": "^4.17.17", + "@types/node": "^20.4.9", + "prisma": "^5.5.2", + "ts-node": "^10.9.1", + "typescript": "^5.1.6" + } +} diff --git a/pulse/fullstack-simple-chat/server/prisma/schema.prisma b/pulse/fullstack-simple-chat/server/prisma/schema.prisma new file mode 100644 index 000000000000..9884ca939159 --- /dev/null +++ b/pulse/fullstack-simple-chat/server/prisma/schema.prisma @@ -0,0 +1,19 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Message { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + text String? + senderSocketId String? +} diff --git a/pulse/fullstack-simple-chat/server/src/index.ts b/pulse/fullstack-simple-chat/server/src/index.ts new file mode 100644 index 000000000000..deaf3e1b6918 --- /dev/null +++ b/pulse/fullstack-simple-chat/server/src/index.ts @@ -0,0 +1,61 @@ +import * as socket from "socket.io"; +import { Server } from "socket.io"; +import express, { Request, Response } from "express"; +import http from "http"; +import cors from "cors"; +import { PrismaClient } from "@prisma/client"; +import { withPulse } from "@prisma/extension-pulse"; + +const prisma = new PrismaClient().$extends( + withPulse({ + apiKey: process.env.PULSE_API_KEY || "", + }) +); + +const app = express(); +app.use(cors()); + +app.get(`/messages`, async (_: Request, res: Response) => { + const messages = await prisma.message.findMany(); + res.json(messages); +}); + +const server = http.createServer(app); + +const io = new socket.Server(server, { + cors: { origin: true }, +}); + +io.on(`connection`, async (socket) => { + console.log(`User connected: ${socket.id}`); + + socket.on(`disconnect`, () => { + console.log(`User disconnected: ${socket.id}`); + }); + + socket.on(`chat-message`, async (text) => { + console.log(`Received message: ${text} (${socket.id})`); + await prisma.message.create({ + data: { + text, + senderSocketId: socket.id, + }, + }); + }); +}); + +server.listen(4000, async () => { + console.log(`Server running on http://localhost:4000`); + await streamChatMessages(io); +}); + +async function streamChatMessages(io: Server) { + console.log(`Stream new messages with Prisma Client ...`); + const stream = await prisma.message.stream({ create: {} }); + + // Handle Prisma stream events + for await (const event of stream) { + console.log(`New event from Pulse: `, event); + io.sockets.emit("chat-message", event.created); + } +} diff --git a/pulse/fullstack-simple-chat/server/tsconfig.json b/pulse/fullstack-simple-chat/server/tsconfig.json new file mode 100644 index 000000000000..afe0bc82e135 --- /dev/null +++ b/pulse/fullstack-simple-chat/server/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + "outDir": "./dist", + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/pulse/starter/README.md b/pulse/starter/README.md index fbb76da34781..9edc4f5eed99 100644 --- a/pulse/starter/README.md +++ b/pulse/starter/README.md @@ -1,6 +1,6 @@ # Prisma Pulse Example: Starter -This repository has been created to help you get started with [Pulse](https://prisma.io/pulse). You will be able to use this project with any Pulse-ready PostgreSQL database. This project comes with a basic [`schema.prisma`](./prisma/schema.prisma) file as well as a Pulse subscription found in the [`index.ts`](./index.ts) file. +This repository has been created to help you get started with [Pulse](https://prisma.io/pulse). You will be able to use this project with any Pulse-ready PostgreSQL database. This project comes with a basic [`schema.prisma`](./prisma/schema.prisma) file as well as a Pulse stream found in the [`index.ts`](./index.ts) file. ## Prerequisites @@ -49,29 +49,25 @@ npx prisma migrate dev --name init You now have an empty `User` table in your database. -### 4. Start the Pulse subscription +### 4. Start the Pulse stream -Run the [script](./index.ts) that contains the code to subscribe to database events: +Run the [script](./index.ts) that contains the code to stream database events: ```bash npx ts-node index.ts ``` -This will run a basic subscription on the `User` table. Whenever a record is created, updated or deleted in that table, an event will fire and the script will execute a `console.log` statement with details of the event it received. +This will create a basic stream on the `User` table. Whenever a record is created, updated or deleted in that table, an event will fire and the script will execute a `console.log` statement with details of the event it received. -The code can be found in the [`index.ts`](./index.ts) file. To learn more about the Pulse API and how to use it, check out our [documentation](https://www.prisma.io/docs/data-platform/pulse/api-reference#subscribe). +The code can be found in the [`index.ts`](./index.ts) file. To learn more about the Pulse API and how to use it, check out our [documentation](https://www.prisma.io/docs/data-platform/pulse/api-reference#stream). -
Pulse subscription on the `User` table +
Pulse stream on the `User` table ```ts async function main() { - const subscription = await prisma.user.subscribe() + const stream = await prisma.user.stream() - if (subscription instanceof Error) { - throw subscription - } - - for await (const event of subscription) { + for await (const event of stream) { console.log('just received an event:', event) } } @@ -79,7 +75,7 @@ async function main() {
-### 5. Test the subscription +### 5. Test the stream The following instructions use [Prisma Studio](https://www.prisma.io/studio) to create a new record in the `User` table. However, you can use any other method to write to the `User` table (e.g. a SQL client like `psql` or [TablePlus](https://tableplus.com/)) in order to trigger a database change event in Pulse. diff --git a/pulse/starter/index.ts b/pulse/starter/index.ts index 21c5b534d059..0a6dd7ddbb5e 100644 --- a/pulse/starter/index.ts +++ b/pulse/starter/index.ts @@ -12,17 +12,13 @@ const prisma = new PrismaClient().$extends( ) async function main() { - const subscription = await prisma.user.subscribe() + const stream = await prisma.user.stream() process.on('exit', (code) => { - subscription.stop() + stream.stop() }) - if (subscription instanceof Error) { - throw subscription - } - - for await (const event of subscription) { + for await (const event of stream) { console.log('just received an event:', event) } } diff --git a/pulse/starter/package.json b/pulse/starter/package.json index 20ba01ddac05..4dc29db7a5b8 100644 --- a/pulse/starter/package.json +++ b/pulse/starter/package.json @@ -12,11 +12,11 @@ "license": "MIT", "dependencies": { "@prisma/client": "5.14.0", - "@prisma/extension-pulse": "1.0.2", + "@prisma/extension-pulse": "dev", "@types/node": "20.12.12", "dotenv": "16.4.5", "prisma": "5.14.0", "ts-node": "10.9.2", "typescript": "5.4.5" } -} \ No newline at end of file +}