Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Prisma support #112

Merged
merged 1 commit into from
Feb 27, 2024
Merged

feat: Prisma support #112

merged 1 commit into from
Feb 27, 2024

Conversation

zermelo-wisen
Copy link
Contributor

Fixes #79

@zermelo-wisen zermelo-wisen marked this pull request as draft February 10, 2024 08:31
@dustinbyrne dustinbyrne self-requested a review February 12, 2024 15:20
}

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we configure this? We have internal services which share a pre-configured, pre-built Prisma client, shipped as its own module. Because of this, we don't ever import @prisma/client directly.

Copy link
Contributor

@dustinbyrne dustinbyrne Feb 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's worth noting that I've tried this PR with our own service after modifying this line and it does work great. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll make the module name (id) a configuration parameter, if I understand correctly.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd double check the unit of time here. I tried a request which issued three queries taking 2ms, 0ms, 1ms according to the logs. The AppMap states the duration was 2000ms, 0ms, 1000ms.

@zermelo-wisen zermelo-wisen force-pushed the feat/prisma-support branch 3 times, most recently from ef55baa to 0a36fa0 Compare February 13, 2024 19:13
Copy link
Collaborator

@dividedmind dividedmind left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job figuring this out! Just a couple of changes I'd suggest before merging. A mention in the README about support for custom prisma client locations would be nice, too.

generator: false,
id: action,
params: params,
location: { path: "unknown", lineno: -1 },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's generate location as @prisma/client/${model} (or custom client name, eg. appmap-prisma-client/${model}) — this will make it look much nicer in sequence diagrams. Also, the line numbers for different signatures should be different — I believe location is the key by which code objects are identified in the clients.

const call = recording.sqlQuery(dbType, queryEvent.query);
const elapsedMs = queryEvent.duration / 1000.0;
recording.functionReturn(call.id, undefined, elapsedMs);
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's pretty clever :) Great job figuring this out!

assert("$on" in thisArg && typeof thisArg.$on === "function");
thisArg.$on("query", (queryEvent: QueryEvent) => {
const call = recording.sqlQuery(dbType, queryEvent.query);
const elapsedMs = queryEvent.duration / 1000.0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this should be called elapsedSec — the doc says duration already is in ms.

const functionInfos = new Map<string, FunctionInfo>();

function getFunctionInfo(model: string, action: string, params: ESTree.Parameter[]) {
const key = model + "." + action;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for different calls to the same action to be parametrized differently? If so, this will create just one signature for all of them which will be correct only for the first one. In this case it might make more sense to pass them to the map as a single argument, or explode them all into (data, include, where) filling in undefined where one isn't provided, to avoid having multiple signatures for the same thing in the AppMap (note they would also need to differ in location because that's how the client code distinguishes code objects).

src/loader.ts Outdated
@@ -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))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should take custom clients into account.


main()
.then(async () => {
await prisma.$disconnect();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't it auto-disconnect? Maybe this then/catch is not necessary.

@zermelo-wisen zermelo-wisen force-pushed the feat/prisma-support branch 4 times, most recently from 1c00d6e to 13a2735 Compare February 14, 2024 13:00
@dustinbyrne dustinbyrne marked this pull request as ready for review February 14, 2024 15:19
src/loader.ts Outdated
@@ -44,7 +45,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", config.prismaClientModuleIds].includes(url))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a splat.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh exactly, thank you.


integrationTest("mapping Prisma tests", () => {
const testDir = `${cwd()}/test/prisma`;
copyFileSync(`${testDir}/original.db`, `${testDir}/test.db`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to copy this over instead of creating it in situ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

original.db consists of the required schema with empty tables. I didn't want to create it in every test run because creating it is not related to the test. test.db gets modified in every run so I wanted to provide a predefined db state for the test.

import { spawnSync } from "node:child_process";

integrationTest("mapping Prisma tests", () => {
const testDir = `${cwd()}/test/prisma`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not very robust — what if jest is run from a different directory? You can use resolveTarget() to get it.

requestParams.action,
["data", "include", "where"].map((k) => {
return { type: "Identifier", name: k } as ESTree.Identifier;
}),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note this can be a constant in getFunctionInfo now.

@dividedmind dividedmind merged commit 93242e1 into main Feb 27, 2024
6 checks passed
@dividedmind dividedmind deleted the feat/prisma-support branch February 27, 2024 14:14
@appland-release
Copy link

🎉 This PR is included in version 2.16.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Prisma support
4 participants