import assert from 'node:assert/strict'
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
import { prismaQueryInsights } from '@prisma/sqlcommenter-query-insights'
import { Prisma, PrismaClient } from './generated/prisma/client.js'
// Helper to decode base64url and parse JSON
function decodePayload(base64url: string): unknown {
const json = Buffer.from(base64url, 'base64url').toString('utf-8')
return JSON.parse(json)
}
// Helper to parse prismaQuery comment value
function parsePrismaQuery(value: string): { prefix: string; payload?: unknown } {
const colonIndex = value.indexOf(':')
if (colonIndex === -1) {
return { prefix: value }
}
const prefix = value.slice(0, colonIndex)
const encoded = value.slice(colonIndex + 1)
const payload = decodePayload(encoded)
return { prefix, payload }
}
// Helper to extract prismaQuery from SQL comment
function extractPrismaQuery(sql: string): string | null {
// Match /*...prismaQuery='...'...*/
const match = sql.match(/prismaQuery='((?:[^'\\]|\\.)*)'/s)
if (!match) return null
// Unescape single quotes and URL-decode
return decodeURIComponent(match[1].replace(/\\'/g, "'"))
}
const PARAM_PLACEHOLDER = { $type: 'Param' }
const adapter = new PrismaBetterSqlite3({
url: './dev.db',
})
const capturedQueries: string[] = []
const prisma = new PrismaClient({
adapter,
comments: [prismaQueryInsights()],
log: [{ emit: 'event', level: 'query' }],
})
prisma.$on('query', (event: Prisma.QueryEvent) => {
capturedQueries.push(event.query)
})
// Helper to clear captured queries and return what was captured
function flushQueries(): string[] {
const queries = [...capturedQueries]
capturedQueries.length = 0
return queries
}
// Helper to verify queries from an operation
function verifyQueries(
description: string,
queries: string[],
checks: {
expectedCount?: number
prefix?: string
queryIndex?: number
containsInPayload?: (payload: unknown) => boolean
notContains?: string[]
},
) {
const queryIndex = checks.queryIndex ?? 0
if (checks.expectedCount !== undefined) {
assert.strictEqual(
queries.length,
checks.expectedCount,
`${description}: expected ${checks.expectedCount} queries, got ${queries.length}`,
)
}
const sql = queries[queryIndex]
assert(sql, `${description}: query at index ${queryIndex} not found (got ${queries.length} queries)`)
const prismaQuery = extractPrismaQuery(sql)
assert(prismaQuery, `${description}: should have prismaQuery comment`)
const parsed = parsePrismaQuery(prismaQuery)
if (checks.prefix) {
assert.strictEqual(parsed.prefix, checks.prefix, `${description}: prefix mismatch`)
}
if (checks.containsInPayload) {
assert(
checks.containsInPayload(parsed.payload),
`${description}: payload check failed: ${JSON.stringify(parsed.payload, null, 2)}`,
)
}
if (checks.notContains) {
for (const value of checks.notContains) {
assert(!prismaQuery.includes(value), `${description}: should not contain "${value}"`)
}
}
console.log(`✓ ${description}`)
}
// We need to store the user id for the post creation test
let createdUserId: number
// Test 1: Simple findMany - should have Model.action format with empty payload (all scalars)
{
console.log('Test 1: Simple findMany')
await prisma.user.findMany()
verifyQueries('findMany', flushQueries(), {
expectedCount: 1,
prefix: 'User.findMany',
containsInPayload: (p) => {
const payload = p as Record<string, unknown>
return Object.keys(payload).length === 0
},
})
}
// Test 2: Create with data - data values should be parameterized
{
console.log('Test 2: Create with data')
const user = await prisma.user.create({
data: { email: 'secret@private.com', name: 'Secret Name' },
})
createdUserId = user.id
verifyQueries('create with data', flushQueries(), {
expectedCount: 1,
prefix: 'User.createOne',
containsInPayload: (p) => {
const payload = p as { data?: { email?: unknown; name?: unknown } }
const data = payload.data
return (
data !== undefined &&
JSON.stringify(data.email) === JSON.stringify(PARAM_PLACEHOLDER) &&
JSON.stringify(data.name) === JSON.stringify(PARAM_PLACEHOLDER)
)
},
notContains: ['secret@private.com', 'Secret Name'],
})
}
// Test 3: findFirst with where clause - filter values should be parameterized
{
console.log('Test 3: findFirst with where clause')
await prisma.user.findFirst({
where: { email: 'secret@private.com' },
})
verifyQueries('findFirst with where', flushQueries(), {
expectedCount: 1,
prefix: 'User.findFirst',
containsInPayload: (p) => {
const payload = p as { where?: { email?: unknown } }
return JSON.stringify(payload.where?.email) === JSON.stringify(PARAM_PLACEHOLDER)
},
notContains: ['secret@private.com'],
})
}
// Test 4: findMany with pagination - take/skip should be preserved
{
console.log('Test 4: findMany with pagination')
await prisma.user.findMany({
take: 10,
skip: 5,
})
verifyQueries('findMany with pagination', flushQueries(), {
expectedCount: 1,
prefix: 'User.findMany',
containsInPayload: (p) => {
const payload = p as { take?: number; skip?: number }
return payload.take === 10 && payload.skip === 5
},
})
}
// Test 5: findMany with orderBy - sort direction should be preserved
{
console.log('Test 5: findMany with orderBy')
await prisma.user.findMany({
orderBy: { createdAt: 'desc' },
})
verifyQueries('findMany with orderBy', flushQueries(), {
expectedCount: 1,
prefix: 'User.findMany',
containsInPayload: (p) => {
const payload = p as { orderBy?: { createdAt?: string } }
return payload.orderBy?.createdAt === 'desc'
},
})
}
// Test 6: Create post with relation - nested data should be parameterized
{
console.log('Test 6: Create with relation')
await prisma.post.create({
data: {
title: 'Secret Title',
content: 'Secret Content',
authorId: createdUserId,
},
})
verifyQueries('create post', flushQueries(), {
expectedCount: 1,
prefix: 'Post.createOne',
containsInPayload: (p) => {
const payload = p as { data?: { title?: unknown; content?: unknown } }
const data = payload.data
return (
data !== undefined &&
JSON.stringify(data.title) === JSON.stringify(PARAM_PLACEHOLDER) &&
JSON.stringify(data.content) === JSON.stringify(PARAM_PLACEHOLDER)
)
},
notContains: ['Secret Title', 'Secret Content'],
})
}
// Test 7: findMany with include - relation selection should use include
// Note: include generates 2 SQL queries (one for User, one for Post), both with the same comment
{
console.log('Test 7: findMany with include')
await prisma.user.findMany({
include: { posts: true },
})
verifyQueries('findMany with include', flushQueries(), {
expectedCount: 2,
queryIndex: 0,
prefix: 'User.findMany',
containsInPayload: (p) => {
const payload = p as { include?: { posts?: unknown } }
return payload.include?.posts === true
},
})
}
// Test 8: Complex where with AND/OR - all values should be parameterized
{
console.log('Test 8: Complex where with AND/OR')
await prisma.user.findMany({
where: {
AND: [{ email: { contains: '@private.com' } }, { OR: [{ name: 'Alice' }, { name: 'Bob' }] }],
},
})
verifyQueries('complex where', flushQueries(), {
expectedCount: 1,
prefix: 'User.findMany',
containsInPayload: (p) => {
const payload = p as {
where?: { AND?: Array<{ email?: { contains?: unknown }; OR?: Array<{ name?: unknown }> }> }
}
const and = payload.where?.AND
if (!and || !Array.isArray(and)) return false
// Check email.contains is parameterized
const emailCondition = and[0]?.email?.contains
if (JSON.stringify(emailCondition) !== JSON.stringify(PARAM_PLACEHOLDER)) return false
// Check OR conditions are parameterized
const orConditions = and[1]?.OR
if (!orConditions || !Array.isArray(orConditions)) return false
return orConditions.every((cond) => JSON.stringify(cond.name) === JSON.stringify(PARAM_PLACEHOLDER))
},
notContains: ['@private.com', 'Alice', 'Bob'],
})
}
// Test 9: findMany with select - verify select is in payload
{
console.log('Test 9: findMany with select')
await prisma.user.findMany({
select: { id: true, email: true },
})
verifyQueries('findMany with select', flushQueries(), {
expectedCount: 1,
prefix: 'User.findMany',
containsInPayload: (p) => {
const payload = p as { select?: { id?: boolean; email?: boolean } }
return payload.select?.id === true && payload.select?.email === true
},
})
}
await prisma.$disconnect()
console.log('\n✅ SQL commenter query insights e2e test passed!')