Skip to content

Commit

Permalink
feat: Prisma support
Browse files Browse the repository at this point in the history
  • Loading branch information
zermelo-wisen committed Feb 9, 2024
1 parent d8200c8 commit c392a63
Show file tree
Hide file tree
Showing 28 changed files with 910 additions and 3 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"devDependencies": {
"@jest/types": "^29.6.3",
"@prisma/client": "^5.9.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@swc/core": "^1.3.78",
Expand Down Expand Up @@ -85,6 +86,7 @@
"test/postgres",
"test/sqlite",
"test/next",
"test/typescript-esm"
"test/typescript-esm",
"test/prisma"
]
}
124 changes: 124 additions & 0 deletions src/hooks/prisma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import assert from "node:assert";

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

import type AppMap from "../AppMap";
import { getTime } from "../util/getTime";
import { fixReturnEventIfPromiseResult, recording } from "../recorder";
import { FunctionInfo } from "../registry";

export default function prismaHook(mod: typeof prisma) {
assert("PrismaClient" in 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 as (...args: unknown[]) => unknown);
return mod;
}

prismaHook.applicable = function (id: string) {
return ["@prisma/client"].includes(id);
};

// https://github.com/prisma/prisma/blob/095cba1a1b79d0d950246b07c9fb48d22fd7f229/packages/client/src/runtime/getPrismaClient.ts#L181
interface QueryEvent {
timestamp: Date;
query: string;
params: string;
duration: number;
target: string;
}

interface PrismaRequestParamsArgs {
data?: unknown;
include?: unknown;
where?: unknown;
}

interface PrismaRequestParams {
action?: string;
model?: string;
args?: PrismaRequestParamsArgs;
}

let hookAttached = false;

function createProxy<T extends (...args: unknown[]) => unknown>(prismaClientMethod: T) {
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);
recording.functionReturn(call.id, undefined, queryEvent.duration);
});
}

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

const params =
requestParams.args != null
? Object.keys(requestParams.args).map((k) => {
return { type: "Identifier", name: k } as ESTree.Identifier;
})
: [];

if (requestParams.action && requestParams.model) {
const info: FunctionInfo = {
async: true,
generator: false,
id: requestParams.action,
params: params,
location: { path: "unknown", lineno: 0 },
klassOrFile: requestParams.model,
static: true,
};
prismaCall = recording.functionCall(
info,
undefined,
params.map((p) => requestParams.args?.[p.name as keyof PrismaRequestParamsArgs]),
);
}
}

if (prismaCall) {
const start = getTime();
try {
const result = target.apply(thisArg, argArray);
const ret = recording.functionReturn(prismaCall.id, result, getTime() - start);
return fixReturnEventIfPromiseResult(result, ret, prismaCall, start);
} catch (exn: unknown) {
recording.functionException(prismaCall.id, exn, getTime() - start);
throw exn;
}
}

return target.apply(thisArg, argArray);
},
});
}
3 changes: 2 additions & 1 deletion src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export const load: NodeLoaderHooksAPI2["load"] = async function load(url, contex
// For these modules, we preempt import with CommonJS require
// to allow our hooks to modify the loaded module in cache
// (which is shared between ESM and CJS for builtins at least).
if (["node:http", "node:https", "http", "https"].includes(url)) forceRequire(url);
if (["node:http", "node:https", "http", "https", "@prisma/client"].includes(url))
forceRequire(url);

return defaultLoad(url, context, defaultLoad);
};
3 changes: 2 additions & 1 deletion src/requireHook.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import httpHook from "./hooks/http";
import mysqlHook from "./hooks/mysql";
import pgHook from "./hooks/pg";
import prismaHook from "./hooks/prisma";
import sqliteHook from "./hooks/sqlite";

interface Hook {
Expand All @@ -9,7 +10,7 @@ interface Hook {
applicable(id: string): boolean;
}

const hooks: Hook[] = [httpHook, mysqlHook, pgHook, sqliteHook];
const hooks: Hook[] = [httpHook, mysqlHook, pgHook, sqliteHook, prismaHook];

export default function requireHook(
original: NodeJS.Require,
Expand Down
Loading

0 comments on commit c392a63

Please sign in to comment.