Skip to content

Commit 1c7d44b

Browse files
feat: add Prisma 7 SQLCommenter demo app
Full-stack Express + React demo showing Prisma 7's native `comments` API with `@prisma/sqlcommenter-query-tags`. Includes proxy-based call-site capture for the `file` tag (path:line:column) and `withQueryTags` middleware for route/method injection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d603f01 commit 1c7d44b

32 files changed

Lines changed: 2085 additions & 0 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DATABASE_URL="postgresql://postgres:password@127.0.0.1:5432/prisma_demo"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
dist/
3+
.env
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Prisma SQLCommenter Demo
2+
3+
A full-stack demo app (Express + React) showing how to use Prisma 7's built-in SQLCommenter support to annotate every SQL query with contextual metadata — source file, HTTP route, request method, model, and action.
4+
5+
The app is a project/issue tracker (like a mini Linear) that generates a variety of OLTP queries: CRUD, aggregations, filtering, pagination, and batch operations.
6+
7+
## Tags produced
8+
9+
Every query gets a SQL comment like:
10+
11+
```sql
12+
SELECT ... FROM "Issue" WHERE ...
13+
/*action='findMany',db_driver='prisma',file='server/routes/issues.ts:36:18',method='GET',model='Issue',route='/api/issues'*/
14+
```
15+
16+
| Tag | Source |
17+
| ----------- | ----------------------------------------- |
18+
| `db_driver` | Custom `SqlCommenterPlugin` |
19+
| `model` | Custom `SqlCommenterPlugin` (from context)|
20+
| `action` | Custom `SqlCommenterPlugin` (from context)|
21+
| `route` | `withQueryTags` middleware (ALS) |
22+
| `method` | `withQueryTags` middleware (ALS) |
23+
| `file` | Proxy-based stack capture at call site |
24+
25+
## Setup
26+
27+
```bash
28+
# Copy environment file
29+
cp .env.example .env
30+
# Edit .env with your PostgreSQL connection string
31+
32+
# Install dependencies
33+
npm install
34+
35+
# Run migrations and seed
36+
npx prisma migrate dev
37+
npm run db:seed
38+
39+
# Start dev server (Express + Vite)
40+
npm run dev
41+
```
42+
43+
The app runs at http://localhost:5173 (frontend) with the API on http://localhost:3456.
44+
45+
## How the `file` tag works
46+
47+
Prisma uses lazy `PrismaPromise` objects — the query doesn't execute at `prisma.issue.findMany()` but later when `.then()` is called by `await`. By that point, user code is no longer on the call stack, so the `SqlCommenterPlugin` can't capture the source file automatically.
48+
49+
This demo solves it by proxying each `prisma.<model>.<method>()` call to:
50+
51+
1. Capture the stack trace at call time (where user code IS on the stack)
52+
2. Extract the file path, line, and column from the first application frame
53+
3. Return a custom thenable that wraps execution inside `withMergedQueryTags({ file })`, merging the file tag with existing route/method tags from the Express middleware
54+
55+
See `server/db.ts` for the implementation.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>DevTracker - Prisma SQLCommenter Demo</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "prisma-demo",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "concurrently --names server,client -c blue,green \"npm run dev:server\" \"npm run dev:client\"",
8+
"dev:server": "tsx watch server/index.ts",
9+
"dev:client": "vite",
10+
"db:migrate": "prisma migrate dev",
11+
"db:seed": "tsx prisma/seed.ts",
12+
"db:reset": "prisma migrate reset --force && tsx prisma/seed.ts",
13+
"build": "vite build",
14+
"postinstall": "prisma generate"
15+
},
16+
"dependencies": {
17+
"@prisma/adapter-pg": "^7.1.0",
18+
"@prisma/client": "^7.1.0",
19+
"@prisma/sqlcommenter": "^7.1.0",
20+
"@prisma/sqlcommenter-query-tags": "^7.1.0",
21+
"cors": "^2.8.5",
22+
"express": "^4.21.0",
23+
"pg": "^8.13.0",
24+
"react": "^19.0.0",
25+
"react-dom": "^19.0.0",
26+
"react-router-dom": "^7.3.0"
27+
},
28+
"devDependencies": {
29+
"@types/cors": "^2.8.17",
30+
"@types/express": "^5.0.0",
31+
"@types/node": "^22.0.0",
32+
"@types/pg": "^8.11.0",
33+
"@types/react": "^19.0.0",
34+
"@types/react-dom": "^19.0.0",
35+
"@vitejs/plugin-react": "^4.3.0",
36+
"concurrently": "^9.1.0",
37+
"prisma": "^7.1.0",
38+
"tsx": "^4.19.0",
39+
"typescript": "^5.7.0",
40+
"vite": "^6.2.0"
41+
}
42+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
-- CreateEnum
2+
CREATE TYPE "IssueStatus" AS ENUM ('OPEN', 'IN_PROGRESS', 'IN_REVIEW', 'CLOSED');
3+
4+
-- CreateEnum
5+
CREATE TYPE "Priority" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'CRITICAL');
6+
7+
-- CreateTable
8+
CREATE TABLE "User" (
9+
"id" TEXT NOT NULL,
10+
"name" TEXT NOT NULL,
11+
"email" TEXT NOT NULL,
12+
"avatarUrl" TEXT,
13+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
14+
15+
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
16+
);
17+
18+
-- CreateTable
19+
CREATE TABLE "Project" (
20+
"id" TEXT NOT NULL,
21+
"name" TEXT NOT NULL,
22+
"key" TEXT NOT NULL,
23+
"description" TEXT,
24+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
25+
"updatedAt" TIMESTAMP(3) NOT NULL,
26+
27+
CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
28+
);
29+
30+
-- CreateTable
31+
CREATE TABLE "Issue" (
32+
"id" TEXT NOT NULL,
33+
"number" INTEGER NOT NULL,
34+
"title" TEXT NOT NULL,
35+
"description" TEXT,
36+
"status" "IssueStatus" NOT NULL DEFAULT 'OPEN',
37+
"priority" "Priority" NOT NULL DEFAULT 'MEDIUM',
38+
"projectId" TEXT NOT NULL,
39+
"assigneeId" TEXT,
40+
"creatorId" TEXT NOT NULL,
41+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
42+
"updatedAt" TIMESTAMP(3) NOT NULL,
43+
44+
CONSTRAINT "Issue_pkey" PRIMARY KEY ("id")
45+
);
46+
47+
-- CreateTable
48+
CREATE TABLE "Label" (
49+
"id" TEXT NOT NULL,
50+
"name" TEXT NOT NULL,
51+
"color" TEXT NOT NULL,
52+
53+
CONSTRAINT "Label_pkey" PRIMARY KEY ("id")
54+
);
55+
56+
-- CreateTable
57+
CREATE TABLE "Comment" (
58+
"id" TEXT NOT NULL,
59+
"body" TEXT NOT NULL,
60+
"issueId" TEXT NOT NULL,
61+
"authorId" TEXT NOT NULL,
62+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
63+
"updatedAt" TIMESTAMP(3) NOT NULL,
64+
65+
CONSTRAINT "Comment_pkey" PRIMARY KEY ("id")
66+
);
67+
68+
-- CreateTable
69+
CREATE TABLE "_IssueToLabel" (
70+
"A" TEXT NOT NULL,
71+
"B" TEXT NOT NULL,
72+
73+
CONSTRAINT "_IssueToLabel_AB_pkey" PRIMARY KEY ("A","B")
74+
);
75+
76+
-- CreateIndex
77+
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
78+
79+
-- CreateIndex
80+
CREATE UNIQUE INDEX "Project_key_key" ON "Project"("key");
81+
82+
-- CreateIndex
83+
CREATE UNIQUE INDEX "Issue_projectId_number_key" ON "Issue"("projectId", "number");
84+
85+
-- CreateIndex
86+
CREATE UNIQUE INDEX "Label_name_key" ON "Label"("name");
87+
88+
-- CreateIndex
89+
CREATE INDEX "_IssueToLabel_B_index" ON "_IssueToLabel"("B");
90+
91+
-- AddForeignKey
92+
ALTER TABLE "Issue" ADD CONSTRAINT "Issue_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
93+
94+
-- AddForeignKey
95+
ALTER TABLE "Issue" ADD CONSTRAINT "Issue_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
96+
97+
-- AddForeignKey
98+
ALTER TABLE "Issue" ADD CONSTRAINT "Issue_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
99+
100+
-- AddForeignKey
101+
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_issueId_fkey" FOREIGN KEY ("issueId") REFERENCES "Issue"("id") ON DELETE CASCADE ON UPDATE CASCADE;
102+
103+
-- AddForeignKey
104+
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
105+
106+
-- AddForeignKey
107+
ALTER TABLE "_IssueToLabel" ADD CONSTRAINT "_IssueToLabel_A_fkey" FOREIGN KEY ("A") REFERENCES "Issue"("id") ON DELETE CASCADE ON UPDATE CASCADE;
108+
109+
-- AddForeignKey
110+
ALTER TABLE "_IssueToLabel" ADD CONSTRAINT "_IssueToLabel_B_fkey" FOREIGN KEY ("B") REFERENCES "Label"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Please do not edit this file manually
2+
# It should be added in your version-control system (e.g., Git)
3+
provider = "postgresql"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import path from "node:path";
2+
import type { PrismaConfig } from "prisma";
3+
4+
export default {
5+
earlyAccess: true,
6+
schema: path.join(import.meta.dirname, "schema.prisma"),
7+
migrate: {
8+
async development() {
9+
return {
10+
url: process.env.DATABASE_URL!,
11+
};
12+
},
13+
},
14+
} satisfies PrismaConfig;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
generator client {
2+
provider = "prisma-client-js"
3+
4+
}
5+
6+
datasource db {
7+
provider = "postgresql"
8+
}
9+
10+
model User {
11+
id String @id @default(uuid())
12+
name String
13+
email String @unique
14+
avatarUrl String?
15+
assignedIssues Issue[] @relation("assignee")
16+
createdIssues Issue[] @relation("creator")
17+
comments Comment[]
18+
createdAt DateTime @default(now())
19+
}
20+
21+
model Project {
22+
id String @id @default(uuid())
23+
name String
24+
key String @unique
25+
description String?
26+
issues Issue[]
27+
createdAt DateTime @default(now())
28+
updatedAt DateTime @updatedAt
29+
}
30+
31+
model Issue {
32+
id String @id @default(uuid())
33+
number Int
34+
title String
35+
description String?
36+
status IssueStatus @default(OPEN)
37+
priority Priority @default(MEDIUM)
38+
projectId String
39+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
40+
assigneeId String?
41+
assignee User? @relation("assignee", fields: [assigneeId], references: [id])
42+
creatorId String
43+
creator User @relation("creator", fields: [creatorId], references: [id])
44+
labels Label[]
45+
comments Comment[]
46+
createdAt DateTime @default(now())
47+
updatedAt DateTime @updatedAt
48+
49+
@@unique([projectId, number])
50+
}
51+
52+
enum IssueStatus {
53+
OPEN
54+
IN_PROGRESS
55+
IN_REVIEW
56+
CLOSED
57+
}
58+
59+
enum Priority {
60+
LOW
61+
MEDIUM
62+
HIGH
63+
CRITICAL
64+
}
65+
66+
model Label {
67+
id String @id @default(uuid())
68+
name String @unique
69+
color String
70+
issues Issue[]
71+
}
72+
73+
model Comment {
74+
id String @id @default(uuid())
75+
body String
76+
issueId String
77+
issue Issue @relation(fields: [issueId], references: [id], onDelete: Cascade)
78+
authorId String
79+
author User @relation(fields: [authorId], references: [id])
80+
createdAt DateTime @default(now())
81+
updatedAt DateTime @updatedAt
82+
}

0 commit comments

Comments
 (0)