Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@
"@prisma/config": "workspace:*",
"@prisma/dev": "0.24.14",
"@prisma/engines": "workspace:*",
"@prisma/studio-core": "0.27.3",
"@prisma/studio-core": "0.33.0",
"mysql2": "3.15.3",
"postgres": "3.4.7"
},
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/Studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,10 @@ async function handleStudioBffRequest(payload: unknown, executor: Executor): Pro
return jsonResponse([null, result])
}

if (procedure === 'query-insights') {
return jsonResponse([serializeError(new Error('Executor does not support query insights'))])
}

procedure satisfies undefined

return textResponse('Unknown procedure', 500)
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/src/__tests__/Studio.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,24 @@ describe('Studio BFF', () => {
expect(await response.json()).toEqual([null, [[{ id: 1 }], [{ id: 2 }]]])
})

test('returns an explicit error when query insights are unsupported', async () => {
await startStudioBff({
execute: vi.fn(),
})

const response = await getBffResponse({
procedure: 'query-insights',
})

expect(response.status).toBe(200)
expect(await response.json()).toEqual([
{
message: 'Executor does not support query insights',
name: 'Error',
},
])
})

test('serves the Prisma logo as the favicon', async () => {
await startStudioBff({
execute: vi.fn(),
Expand Down
45 changes: 44 additions & 1 deletion packages/cli/src/__tests__/studio-server.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,56 @@ test('logs server errors and returns the error message in the response body', as
expect(consoleErrorSpy).toHaveBeenCalledWith('[Prisma Studio]', error)
})

async function startTestServer(handler: (request: Request) => Response | Promise<Response>): Promise<{ port: number }> {
test('does not log when the client disconnects before the response is written', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
let resolveHandlerStarted!: () => void
let resolveRequestDestroyed!: () => void
let resolveRequestSettled!: () => void
let resolveResponse!: () => void
const handlerStarted = new Promise<void>((resolve) => {
resolveHandlerStarted = resolve
})
const requestDestroyed = new Promise<void>((resolve) => {
resolveRequestDestroyed = resolve
})
const requestSettled = new Promise<void>((resolve) => {
resolveRequestSettled = resolve
})
const responseReady = new Promise<void>((resolve) => {
resolveResponse = resolve
})
const { port } = await startTestServer(async (request) => {
resolveHandlerStarted()
request.signal.addEventListener('abort', resolveRequestDestroyed, { once: true })
await responseReady
return new Response('late response')
}, resolveRequestSettled)
const abortController = new AbortController()
const responsePromise = fetch(`http://127.0.0.1:${port}/`, {
signal: abortController.signal,
}).catch((error: unknown) => error)

await handlerStarted
abortController.abort()
await requestDestroyed
resolveResponse()

await expect(responsePromise).resolves.toMatchObject({ name: 'AbortError' })
await requestSettled
expect(consoleErrorSpy).not.toHaveBeenCalled()
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async function startTestServer(
handler: (request: Request) => Response | Promise<Response>,
onNodeRequestSettled?: () => void,
): Promise<{ port: number }> {
const port = await getPort({ host: '127.0.0.1' })

await new Promise<void>((resolve) => {
const server = startStudioServer({
handler,
onListen: resolve,
onNodeRequestSettled,
port,
})

Expand Down
26 changes: 23 additions & 3 deletions packages/cli/src/studio-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface StudioServer {
type StartStudioServerOptions = {
handler: StudioRequestHandler
onListen(): void
onNodeRequestSettled?(): void
port: number
}

Expand All @@ -29,13 +30,22 @@ export function startStudioServer(options: StartStudioServerOptions): StudioServ
}
}

function startNodeStudioServer({ handler, onListen, port }: StartStudioServerOptions): StudioServer {
function startNodeStudioServer({
handler,
onListen,
onNodeRequestSettled,
port,
}: StartStudioServerOptions): StudioServer {
const server = createServer(async (nodeRequest, nodeResponse) => {
try {
const request = createNodeRequest(nodeRequest, port)
const request = createNodeRequest(nodeRequest, nodeResponse, port)
const response = await handler(request)
await writeNodeResponse(nodeResponse, response, nodeRequest.method)
} catch (error) {
if (nodeResponse.destroyed) {
return
}

console.error('[Prisma Studio]', error)

if (nodeResponse.headersSent || nodeResponse.writableEnded) {
Expand All @@ -46,6 +56,8 @@ function startNodeStudioServer({ handler, onListen, port }: StartStudioServerOpt
nodeResponse.statusCode = 500
nodeResponse.setHeader('Access-Control-Allow-Origin', '*')
nodeResponse.end(error instanceof Error ? error.message : 'Internal Server Error')
} finally {
onNodeRequestSettled?.()
}
})

Expand All @@ -58,10 +70,17 @@ function startNodeStudioServer({ handler, onListen, port }: StartStudioServerOpt
}
}

function createNodeRequest(nodeRequest: IncomingMessage, port: number): Request {
function createNodeRequest(nodeRequest: IncomingMessage, nodeResponse: ServerResponse, port: number): Request {
const origin = `http://${nodeRequest.headers.host ?? `localhost:${port}`}`
const url = new URL(nodeRequest.url ?? '/', origin)
const headers = new Headers()
const abortController = new AbortController()

nodeResponse.once('close', () => {
if (!nodeResponse.writableEnded) {
abortController.abort()
}
})

for (const [key, value] of Object.entries(nodeRequest.headers)) {
if (Array.isArray(value)) {
Expand All @@ -76,6 +95,7 @@ function createNodeRequest(nodeRequest: IncomingMessage, port: number): Request
const requestInit: RequestInit & { duplex?: 'half' } = {
headers,
method: nodeRequest.method,
signal: abortController.signal,
}

if (methodHasRequestBody(nodeRequest.method)) {
Expand Down
Loading
Loading