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

The $queryRaw method throws a TypeError when a Prisma Client extension that overrides the $transaction method is used #14

Closed
liu-ronny opened this issue Nov 2, 2024 · 1 comment

Comments

@liu-ronny
Copy link

Issue

When a PrismaTestingHelper instance is instantiated using a Prisma Client extension that overrides the $transaction method, methods like $queryRaw fail with a TypeError.

In my case, I'm overriding the $transaction method to implement automatic retries when Postgres serialization errors occur. The issue seems to arise when a non-writable, non-configurable own data property is requested through a PrismaTestingHelper's internal proxyClient. In this case, the proxy fails to return the actual value.

Example

The below TypeScript example reproduces the issue.

import { PrismaTestingHelper } from '@chax-at/transactional-prisma-testing';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// A contrived example in which the `$transaction` method is overriden
// Here, the extension simply calls the `$transaction` method of the original client
// I do something similar to implement automatic retries on Postgres transaction serialization failures
const xprisma = prisma.$extends({
  client: {
    $transaction(...args: Parameters<typeof prisma.$transaction>) {
      return prisma.$transaction.apply(prisma, args);
    },
  } as { $transaction: typeof prisma.$transaction },
});

const prismaTestingHelper = new PrismaTestingHelper(
  xprisma as unknown as PrismaClient,
);

const runTestTx = async () => {
  try {
    await prismaTestingHelper.startNewTransaction();
    const prismaProxy = prismaTestingHelper.getProxyClient();

    await prismaProxy.$queryRaw`select 1;`;
  } catch (err) {
    console.error(err);
  } finally {
    prismaTestingHelper.rollbackCurrentTransaction();
    await prisma.$disconnect();
    await xprisma.$disconnect();
  }
};

runTestTx();

TypeError: 'get' on proxy: property '_extensions' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '#' but got '#')

Analysis

After stepping through the above example with a debugger, here's what I think is going on.

When prismaProxy.$queryRaw is called, the proxy's _extensions property is accessed somewhere in the call stack. Once that happens, the proxy's get handler is called.

this.proxyClient = new Proxy(prismaClient, {
get(target, prop, receiver) {
if(prismaTestingHelper.currentPrismaTransactionClient == null) {
// No transaction active, relay to original client
return Reflect.get(target, prop, receiver);
}
if(prop === '$transaction') {
return prismaTestingHelper.transactionProxyFunction.bind(prismaTestingHelper);
}
if((prismaTestingHelper.currentPrismaTransactionClient as any)[prop] != null) {
const ret = Reflect.get(prismaTestingHelper.currentPrismaTransactionClient, prop, receiver);
// Check whether the return value looks like a prisma delegate (by checking whether it has a findFirst function)
if(typeof ret === 'object' && 'findFirst' in ret && typeof ret.findFirst === 'function') {
return prismaTestingHelper.getPrismaDelegateProxy(ret);
}
return ret;
}
// The property does not exist on the transaction client, relay to original client
return Reflect.get(target, prop, receiver);
},
});

Because the _extensions property exists, it gets reflected to prismaTestingHelper.currentPrismaTransactionClient. This is problematic because the _extensions property in the currentPrismaTransactionClient is not the same as the one in the original prismaClient provided in the constructor. I'm not exactly sure why.

Potential Solution

If we explicitly check whether a given property is non-configurable, non-writable on the target of the proxy, we can make sure we always relay the get to the original client. When I try adding the check, tests involving $queryRaw or $executeRaw work without throwing an error. I've searched around and found similar code here.

    this.proxyClient = new Proxy(prismaClient, {
      get(target, prop, receiver) {
        // -------------------------------------------------------------------------------
        // Add the following lines
        const descriptor = Object.getOwnPropertyDescriptor(target, prop);

        if (descriptor && !descriptor.configurable && !descriptor.writable) {
          // This property is not configurable and not writable, relay to original client
          return Reflect.get(target, prop, receiver);
        }
        // -------------------------------------------------------------------------------

        if(prismaTestingHelper.currentPrismaTransactionClient == null) {
          // No transaction active, relay to original client
          return Reflect.get(target, prop, receiver);
        }

        if(prop === '$transaction') {
          return prismaTestingHelper.transactionProxyFunction.bind(prismaTestingHelper);
        }

        if((prismaTestingHelper.currentPrismaTransactionClient as any)[prop] != null) {
          const ret = Reflect.get(prismaTestingHelper.currentPrismaTransactionClient, prop, receiver);
          // Check whether the return value looks like a prisma delegate (by checking whether it has a findFirst function)
          if(typeof ret === 'object' && 'findFirst' in ret && typeof ret.findFirst === 'function') {
            return prismaTestingHelper.getPrismaDelegateProxy(ret);
          }

          return ret;
        }
        // The property does not exist on the transaction client, relay to original client
        return Reflect.get(target, prop, receiver);
      },
    });

Thoughts?

@liu-ronny liu-ronny changed the title The $queryRaw method throws a TypeError when Prisma Client extensions are used The $queryRaw method throws a TypeError when a Prisma Client extension that overrides the $transaction method is used Nov 2, 2024
@Valerionn
Copy link
Member

Valerionn commented Nov 2, 2024

Hey!

Thank you for your detailed bug report! I've just released version 1.2.2 with your proposed solution which should fix this issue.

If you're still encountering problems, feel free to re-open this issue or create a new one.

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

No branches or pull requests

2 participants