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

Support Prisma ORM #460

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 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
10 changes: 10 additions & 0 deletions .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ jobs:
"CLICKHOUSE_DEFAULT_ACCESS": "MANAGEMENT=1"
ports:
- "27019:8123"
mongodb-replica:
image: bitnami/mongodb:8.0
env:
MONGODB_ADVERTISED_HOSTNAME: 127.0.0.1
MONGODB_REPLICA_SET_MODE: primary
MONGODB_ROOT_USER: root
MONGODB_ROOT_PASSWORD: password
MONGODB_REPLICA_SET_KEY: replicasetkey123
ports:
- "27020:27017"
strategy:
fail-fast: false
matrix:
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ koa-sqlite3:
fastify-clickhouse:
cd sample-apps/fastify-clickhouse && AIKIDO_DEBUG=true AIKIDO_BLOCKING=true node app.js

.PHONY: hono-prisma
hono-prisma:
cd sample-apps/hono-prisma && AIKIDO_DEBUG=true AIKIDO_BLOCK=true node app.js

.PHONY: install
install:
mkdir -p build
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Zen for Node.js 16+ is compatible with:
* ✅ [`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3) 11.x, 10.x, 9.x and 8.x
* ✅ [`postgres`](https://www.npmjs.com/package/postgres) 3.x
* ✅ [`@clickhouse/client`](https://www.npmjs.com/package/@clickhouse/client) 1.x
* ✅ [`@prisma/client`](https://www.npmjs.com/package/@prisma/client) 5.x

### Cloud providers

Expand Down
129 changes: 129 additions & 0 deletions end2end/tests/hono-prisma.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
const t = require("tap");
const { spawn } = require("child_process");
const { resolve, join } = require("path");
const timeout = require("../timeout");
const { promisify } = require("util");
const { exec: execCb } = require("child_process");

const execAsync = promisify(execCb);

const appDir = resolve(__dirname, "../../sample-apps/hono-prisma");
const pathToApp = join(appDir, "app.js");

process.env.DATABASE_URL = "file:./dev.db";

t.before(async (t) => {
// Generate prismajs client
const { stdout, stderr } = await execAsync(
"npx prisma migrate reset --force", // Generate prisma client, reset db and apply migrations
{
cwd: appDir,
}
);

if (stderr) {
t.fail(stderr);
}
});

t.test("it blocks in blocking mode", (t) => {
const server = spawn(`node`, [pathToApp, "4002"], {
env: { ...process.env, AIKIDO_DEBUG: "true", AIKIDO_BLOCKING: "true" },
});

server.on("close", () => {
t.end();
});

server.on("error", (err) => {
t.fail(err.message);
hansott marked this conversation as resolved.
Show resolved Hide resolved
});

let stdout = "";
server.stdout.on("data", (data) => {
stdout += data.toString();
});

let stderr = "";
server.stderr.on("data", (data) => {
stderr += data.toString();
});

// Wait for the server to start
timeout(2000)
.then(() => {
return Promise.all([
fetch('http://127.0.0.1:4002/posts/Test" OR 1=1 -- C', {
method: "GET",
signal: AbortSignal.timeout(5000),
}),
fetch("http://127.0.0.1:4002/posts/Happy", {
method: "GET",
signal: AbortSignal.timeout(5000),
}),
]);
})
.then(([sqlInjection, normalAdd]) => {
t.equal(sqlInjection.status, 500);
t.equal(normalAdd.status, 200);
t.match(stdout, /Starting agent/);
t.match(stderr, /Zen has blocked an SQL injection/);
})
.catch((error) => {
t.fail(error.message);
hansott marked this conversation as resolved.
Show resolved Hide resolved
})
.finally(() => {
server.kill();
});
});

t.test("it does not block in non-blocking mode", (t) => {
const server = spawn(`node`, [pathToApp, "4002"], {
env: { ...process.env, AIKIDO_DEBUG: "true" },
});

server.on("close", () => {
t.end();
});

server.on("error", (err) => {
t.fail(err.message);
hansott marked this conversation as resolved.
Show resolved Hide resolved
});

let stdout = "";
server.stdout.on("data", (data) => {
stdout += data.toString();
});

let stderr = "";
server.stderr.on("data", (data) => {
stderr += data.toString();
});

// Wait for the server to start
timeout(2000)
.then(() => {
return Promise.all([
fetch('http://127.0.0.1:4002/posts/Test" OR 1=1 -- C', {
method: "GET",
signal: AbortSignal.timeout(5000),
}),
fetch("http://127.0.0.1:4002/posts/Happy", {
method: "GET",
signal: AbortSignal.timeout(5000),
}),
]);
})
.then(([sqlInjection, normalAdd]) => {
t.equal(sqlInjection.status, 200);
t.equal(normalAdd.status, 200);
t.match(stdout, /Starting agent/);
t.notMatch(stderr, /Zen has blocked an SQL injection/);
})
.catch((error) => {
t.fail(error.message);
hansott marked this conversation as resolved.
Show resolved Hide resolved
})
.finally(() => {
server.kill();
});
});
9 changes: 9 additions & 0 deletions library/agent/hooks/wrapExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,16 @@ function inspectArgs(
module: pkgInfo.name,
});
}
onInspectionInterceptorResult(context, agent, result, pkgInfo, start);
}

export function onInspectionInterceptorResult(
context: ReturnType<typeof getContext>,
agent: Agent,
result: InterceptorResult,
pkgInfo: WrapPackageInfo,
start: number
) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

split because of length?

const end = performance.now();
agent.getInspectionStatistics().onInspectedCall({
sink: pkgInfo.name,
Expand Down
70 changes: 70 additions & 0 deletions library/agent/hooks/wrapNewInstance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,73 @@ t.test("Can wrap default export", async (t) => {
// @ts-expect-error Test method is added by interceptor
t.same(instance.testMethod(), "aikido");
});

t.test("Errors in interceptor are caught", async (t) => {
const exports = {
test: class Test {
constructor(private input: string) {}

getInput() {
return this.input;
}
},
};

logger.clear();

wrapNewInstance(exports, "test", { name: "test", type: "external" }, () => {
throw new Error("test error");
});

const instance = new exports.test("input");
t.same(instance.getInput(), "input");
t.same(logger.getMessages(), ["Failed to wrap method test in module test"]);
});

t.test("Return value from interceptor is returned", async (t) => {
const exports = {
test: class Test {
constructor(private input: string) {}

getInput() {
return this.input;
}
},
};

wrapNewInstance(exports, "test", { name: "test", type: "external" }, () => {
return { testMethod: () => "aikido" };
});

const instance = new exports.test("input");
t.same(typeof instance.getInput, "undefined");
// @ts-expect-error Test method is added by interceptor
t.same(instance.testMethod(), "aikido");
});

t.test("Logs error when wrapping default export", async (t) => {
let exports = class Test {
constructor(private input: string) {}

getInput() {
return this.input;
}
};

logger.clear();

exports = wrapNewInstance(
exports,
undefined,
{ name: "test", type: "external" },
() => {
throw new Error("test error");
}
) as any;

const instance = new exports("input");
t.same(instance.getInput(), "input");
t.same(logger.getMessages(), [
"Failed to wrap method default export in module test",
]);
});
14 changes: 12 additions & 2 deletions library/agent/hooks/wrapNewInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function wrapNewInstance(
subject: unknown,
className: string | undefined,
pkgInfo: WrapPackageInfo,
interceptor: (exports: any) => void
interceptor: (exports: any) => void | unknown
) {
const agent = getInstance();
if (!agent) {
Expand All @@ -28,7 +28,17 @@ export function wrapNewInstance(
// @ts-expect-error It's a constructor
const newInstance = new original(...args);

interceptor(newInstance);
try {
const returnVal = interceptor(newInstance);
if (returnVal) {
return returnVal;
}
} catch (error) {
agent.onFailedToWrapMethod(
pkgInfo.name,
className || "default export"
);
}

return newInstance;
};
Expand Down
2 changes: 2 additions & 0 deletions library/agent/protect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { Postgresjs } from "../sinks/Postgresjs";
import { Fastify } from "../sources/Fastify";
import { Koa } from "../sources/Koa";
import { ClickHouse } from "../sinks/ClickHouse";
import { Prisma } from "../sinks/Prisma";

function getLogger(): Logger {
if (isDebugging()) {
Expand Down Expand Up @@ -136,6 +137,7 @@ function getWrappers() {
new Fastify(),
new Koa(),
new ClickHouse(),
new Prisma(),
];
}

Expand Down
Loading
Loading