diff --git a/packages/runtime/src/enhancements/proxy.ts b/packages/runtime/src/enhancements/proxy.ts index 8dd386030..10175502d 100644 --- a/packages/runtime/src/enhancements/proxy.ts +++ b/packages/runtime/src/enhancements/proxy.ts @@ -155,6 +155,9 @@ export class DefaultPrismaProxyHandler implements PrismaProxyHandler { } } +// a marker for filtering error stack trace +const ERROR_MARKER = '__error_marker__'; + /** * Makes a Prisma client proxy. */ @@ -216,9 +219,71 @@ export function makeProxy( return undefined; } - return makeHandler(target, prop); + return createHandlerProxy(makeHandler(target, prop)); }, }); return proxy; } + +// A proxy for capturing errors and processing stack trace +function createHandlerProxy(handler: T): T { + return new Proxy(handler, { + get(target, propKey) { + const prop = target[propKey as keyof T]; + if (typeof prop !== 'function') { + return prop; + } + + // eslint-disable-next-line @typescript-eslint/ban-types + const origMethod = prop as Function; + return async function (...args: any[]) { + const _err = new Error(ERROR_MARKER); + try { + return await origMethod.apply(handler, args); + } catch (err) { + if (_err.stack && err instanceof Error) { + (err as any).internalStack = err.stack; + err.stack = cleanCallStack(_err.stack, propKey.toString(), err.message); + } + throw err; + } + }; + }, + }); +} + +// Filter out @zenstackhq/runtime stack (generated by proxy) from stack trace +function cleanCallStack(stack: string, method: string, message: string) { + // message line + let resultStack = `Error calling enhanced Prisma method \`${method}\`: ${message}`; + + const lines = stack.split('\n'); + let foundMarker = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (!foundMarker) { + // find marker, then stack trace lines follow + if (line.includes(ERROR_MARKER)) { + foundMarker = true; + } + continue; + } + + // skip leading zenstack and anonymous lines + if (line.includes('@zenstackhq/runtime') || line.includes('')) { + continue; + } + + // capture remaining lines + resultStack += lines + .slice(i) + .map((l) => '\n' + l) + .join(); + break; + } + + return resultStack; +} diff --git a/tests/integration/tests/misc/stacktrace.test.ts b/tests/integration/tests/misc/stacktrace.test.ts new file mode 100644 index 000000000..6573ed088 --- /dev/null +++ b/tests/integration/tests/misc/stacktrace.test.ts @@ -0,0 +1,39 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import path from 'path'; + +describe('Stack trace tests', () => { + let origDir: string; + + beforeAll(async () => { + origDir = path.resolve('.'); + }); + + afterEach(() => { + process.chdir(origDir); + }); + + it('stack trace', async () => { + const { withPolicy } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + } + ` + ); + + const db = withPolicy(); + let error: Error | undefined = undefined; + + try { + await db.model.create({ data: {} }); + } catch (err) { + error = err as Error; + } + + expect(error?.stack).toContain( + "Error calling enhanced Prisma method `create`: denied by policy: model entities failed 'create' check" + ); + expect(error?.stack).toContain(`misc/stacktrace.test.ts`); + expect((error as any).internalStack).toBeTruthy(); + }); +});