chax-at / transactional-prisma-testing

Provides an easy way to execute database tests in a transaction that will be rolled back after each test for fast testing
MIT License
38 stars 1 forks source link

Passing an extended PrismaClient breaks PrismaTestingHelper type #10

Closed asfaltboy closed 5 months ago

asfaltboy commented 10 months ago

Using a client with extensions (e.g the prisma-cursorstream extension, but also others) breaks the expected type parameters of PrismaTestingHelper.

Diagnostics:
 Argument of type 'DynamicClientExtensionThis<TypeMap<InternalArgs & { result: {}; model: { $allModels: { cursorStream: () => <T, A extends Args<T, "findMany">, R extends
  Result<T, A, "findMany">[number], C extends ((dataset: R[]) => Promise<...>) | undefined>(this: T, findManyArgs: A, { batchSize, prefill, batchTransformer }?: { ...;...
 ' is not assignable to parameter of type 'PrismaClient<PrismaClientOptions, never, DefaultArgs>'.
 │ Type 'DynamicClientExtensionThis<TypeMap<InternalArgs & { result: {}; model: { $allModels: { cursorStream: () => <T, A extends Args<T, "findMany">, R extends Result<T,
  A, "findMany">[number], C extends ((dataset: R[]) => Promise<...>) | undefined>(this: T, findManyArgs: A, { batchSize, prefill, batchTransformer }?: { ...;...' is missi
 ng the following properties from type 'PrismaClient<PrismaClientOptions, never, DefaultArgs>': $on, $use [2345]

It seems to always be missing $on, $use properties, likely due to this limitation

To work around this I had to cast it to unknown and then to PrismaClient before passing the extended client to the helper, and when getting it back from the proxy:

import cursorStream from 'prisma-cursorstream';

export let prismaClient;
let prismaTestingHelper: PrismaTestingHelper<PrismaClient> | undefined;

beforeEach(async () => {
  if (prismaTestingHelper == null) {
    const originalPrismaClient = new PrismaClient().$extends(cursorStream);
    prismaTestingHelper = new PrismaTestingHelper(
      originalPrismaClient  as unknown as PrismaClient
    );

    const proxyClient = prismaTestingHelper.getProxyClient() as unknown;
    db = proxyClient as typeof originalPrismaClient;
  }

  await prismaTestingHelper.startNewTransaction();
})

Could we narrow the expected object interface somehow?

alexjesp commented 10 months ago

Seems related? https://github.com/prisma/prisma/issues/17948

Valerionn commented 10 months ago

I have changed the types a bit and published a new version that you can install using

npm i -D @chax-at/transactional-prisma-testing@1.1.1-broader-types.1

In my simple example this works - however, I personally don't use client extensions, so could you please verify that this solution works for you? Afterwards I can publicly publish version 1.1.1 with this fix.

asfaltboy commented 10 months ago

Thanks for addressing so quickly! I have tested the above fix version, and types do work correctly now 🎉

That said, I encountered another issue when using the prisma-cursorstream extension in particular, an error appears when I try to use it in a test that is rolled back:

FAIL  src/thing.integration.ts (330 MB heap size)
  ● thing › should format a JSONL stream

    TypeError [ERR_INVALID_ARG_TYPE]: The "iterable" argument must be an instance of Iterable. Received an instance of Object

      105 |   });
      106 |
    > 107 |   return Readable.from(
          |                   ^
      108 |     db.thing.cursorStream(
      109 |       {
      110 |         where: {

      at getStream (src/thing.ts:107:19)
      at async Object.<anonymous> (src/thing.integration.ts:288:24)

I mention this here, because the same code works fine on its own, or when testing and not using PrismaTestingHelper.

Would you possibly have more time to look more into this? Should I create a separate ticket? Any other info I could provide, I'm thinking it would be nice to add some simple test cases perhaps?

Valerionn commented 10 months ago

I think this happens because the PrismaTestingHelper wraps the cursorStream call and assumes it returns a Promise like every other Prisma function, but cursorStream doesn't return a Promise.

You can (temporarily) add await to your code above, something like

return Readable.from(
await db.thing.cursorStream( // ...

to confirm my suspicion. If this fixes the problem, then I can add a denylist to the configuration, so that PrismaTestingHelper won't wrap certain calls like this one (since adding await everywhere is obviously not a good fix).

However, it might be possible that prisma-cursorstream always uses the original client (i.e. will always return an empty result since it's not querying inside the transaction). If this is the case, then the problem is more complicated, and I can't guarantee that I find time to look into this (but a simple test case for this would definitely be helpful if it doesn't return anything).

asfaltboy commented 10 months ago

Hmm, something changed with the await, but I'm not sure what it means:

I'm getting back a Readable instance that looks like this:

    iterable is: Readable {
      _readableState: ReadableState {
        state: 6193,
        highWaterMark: 1,
        buffer: BufferList { head: null, tail: null, length: 0 },
        length: 0,
        pipes: [],
        flowing: null,
        errored: null,
        defaultEncoding: 'utf8',
        awaitDrainWriters: null,
        decoder: null,
        encoding: null,
        [Symbol(kPaused)]: null
      },
      _events: [Object: null prototype] {},
      _eventsCount: 0,
      _maxListeners: undefined,
      _read: [Function (anonymous)],
      _destroy: [Function (anonymous)],
      [Symbol(kCapture)]: false
    }

But trying to access it through iterable.toArray() returns

    TypeError: Cannot read properties of undefined (reading 'findMany')

🤔 sorry I'm not very proficient at Typescript/Javascript so might be missing something obvious?

I'm guessing that by trying to read from the readable, I'm accessing the underlying result of awaiting the cursorStream, which is a call to findMany or something... but why is it undefined?

Update: confirmed that the result of calling await db.thing.cursorStream() in the test, returns undefined

Valerionn commented 10 months ago

That's probably some weird interaction with the cursorstream package. Unfortunately, I currently don't have the time to debug this in detail (especially with Christmas coming up), and I also can't really give you a time frame when I can get to it.

You could try version 0.6.0 as a workaround because it uses a slightly different architecture - but I fear that this won't work either.

If you can provide me a full repository to test this case (i.e. a Repo that I can clone, run npm install, run Prisma push and then simply run npm test to observe the error), then I can try to look at it again in the debugger to find out if it's something that I can fix.

Valerionn commented 5 months ago

I've just released v1.2.0 with the broader types which fixes the typing problems at least and generally allows extended PrismaClients.

I've closed the issue for now - but feel free to reopen it or create a new one if the issue is still relevant and you have a full reproduction repo.