Skip to content

Commit

Permalink
fix: Sql query recording in extended Prisma clients
Browse files Browse the repository at this point in the history
  • Loading branch information
zermelo-wisen committed Apr 6, 2024
1 parent 9e1048a commit 9799fe0
Showing 1 changed file with 56 additions and 34 deletions.
90 changes: 56 additions & 34 deletions src/hooks/prisma.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "node:assert";

import { ESTree } from "meriyah";
import type { ESTree } from "meriyah";
import type prisma from "@prisma/client";

import type * as AppMap from "../AppMap";
Expand All @@ -10,15 +10,39 @@ import { FunctionInfo } from "../registry";
import config from "../config";

export default function prismaHook(mod: typeof prisma, id?: string) {
assert(mod.PrismaClient != null);
// (1) Prisma Queries: We proxy prismaClient._request method in order to record
// prisma queries (not sqls) as appmap function call events.
// (2) SQL Queries: We have to change config parameters (logLevel, logQueries)
// and register a prismaClient.$on("query") handler to record sql queries.
// We have to do it by proxying mod.PrismaClient here, since it turned out that
// it's too late to do it inside the first invocation of the _request method,
// because $on is (becomes?) undefined in extended prisma clients.
// https://www.prisma.io/docs/orm/reference/prisma-client-reference#remarks-37

// Normally, we "Cannot assign to 'PrismaClient' because it is a read-only property."
// Fortunately with this hack it can be done.
const modHacked = mod as unknown as { PrismaClient: unknown };
modHacked.PrismaClient = new Proxy(mod.PrismaClient, {
construct(target, argArray, newTarget): object {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument
const result = Reflect.construct(target, argArray, newTarget);
if (!hookAttached) {
hookAttached = true;
attachSqlHook(result);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return result;
},
});

// Imported PrismaClient type does not have _request method in type definition.
// But we have it in runtime.
console.log("MOD", mod);
assert(mod.PrismaClient != null);
const PC = mod.PrismaClient as { prototype: unknown };
const proto = PC.prototype;
assert(proto != null && typeof proto === "object");
assert("_request" in proto);
proto._request = createProxy(
proto._request = createPrismaClientMethodProxy(
proto._request as (...args: unknown[]) => unknown,
id ?? "@prisma/client",
);
Expand Down Expand Up @@ -77,45 +101,16 @@ function getFunctionInfo(model: string, action: string, moduleId: string) {
return functionInfos.get(key)!;
}

function createProxy<T extends (...args: unknown[]) => unknown>(
function createPrismaClientMethodProxy<T extends (...args: unknown[]) => unknown>(
prismaClientMethod: T,
moduleId: string,
) {
return new Proxy(prismaClientMethod, {
apply(target, thisArg: unknown, argArray: Parameters<T>) {
if (!hookAttached) {
hookAttached = true;
assert(
thisArg != null &&
typeof thisArg === "object" &&
"_engine" in thisArg &&
thisArg._engine != null &&
typeof thisArg._engine === "object" &&
"config" in thisArg._engine &&
thisArg._engine.config != null &&
typeof thisArg._engine.config === "object" &&
"logLevel" in thisArg._engine.config &&
"logQueries" in thisArg._engine.config &&
"activeProvider" in thisArg._engine.config &&
typeof thisArg._engine.config.activeProvider == "string",
);

const dbType = thisArg._engine.config.activeProvider;
thisArg._engine.config.logLevel = "query";
thisArg._engine.config.logQueries = true;
assert("$on" in thisArg && typeof thisArg.$on === "function");
thisArg.$on("query", (queryEvent: QueryEvent) => {
const call = recording.sqlQuery(dbType, queryEvent.query);
const elapsedSec = queryEvent.duration / 1000.0;
recording.functionReturn(call.id, undefined, elapsedSec);
});
}

// Report Prisma query as a function call, if suitable
let prismaCall: AppMap.FunctionCallEvent | undefined;
if (argArray?.length > 0) {
const requestParams = argArray[0] as PrismaRequestParams;

if (requestParams.action && requestParams.model) {
prismaCall = recording.functionCall(
getFunctionInfo(requestParams.model, requestParams.action, moduleId),
Expand All @@ -141,3 +136,30 @@ function createProxy<T extends (...args: unknown[]) => unknown>(
},
});
}

function attachSqlHook(thisArg: unknown) {
assert(
thisArg != null &&
typeof thisArg === "object" &&
"_engine" in thisArg &&
thisArg._engine != null &&
typeof thisArg._engine === "object" &&
"config" in thisArg._engine &&
thisArg._engine.config != null &&
typeof thisArg._engine.config === "object" &&
"logLevel" in thisArg._engine.config &&
"logQueries" in thisArg._engine.config &&
"activeProvider" in thisArg._engine.config &&
typeof thisArg._engine.config.activeProvider == "string",
);

const dbType = thisArg._engine.config.activeProvider;
thisArg._engine.config.logLevel = "query";
thisArg._engine.config.logQueries = true;
assert("$on" in thisArg && typeof thisArg.$on === "function");
thisArg.$on("query", (queryEvent: QueryEvent) => {
const call = recording.sqlQuery(dbType, queryEvent.query);
const elapsedSec = queryEvent.duration / 1000.0;
recording.functionReturn(call.id, undefined, elapsedSec);
});
}

0 comments on commit 9799fe0

Please sign in to comment.