typescriptpromiseprisma

Transforming Prisma Queries for $transaction with Custom Mappers


I'm using Prisma ORM and frequently utilize the $transaction method. I want to create reusable prebuilt queries that automatically map their results, allowing me to avoid calling the mapper function each time I run a query.

Here's an example of a prebuilt query I want to create:

export async function getLatestArticles(date = new Date()) {
  return prismaClient.articles.findMany({
    orderBy: { date: "desc" },
    take: 20,
    where: { date: { gt: date } },
  });

To achieve automatic mapping, I tried implementing the following approach:

export async function getLatestArticles(date = new Date()) {
  return prismaClient.articles
    .findMany({
      orderBy: { date: "desc" },
      take: 20,
      where: { date: { gt: date } },
    })
    .then((articles) => articles.map(articlesMapper));
}

However, when I use this query inside prismaClient.$transaction, I encounter the following error:

All elements of the array need to be Prisma Client promises. Hint: Please make sure you are not awaiting the Prisma client calls you intended to pass in the $transaction function.

To work around this, I created a wrapper class to trick Prisma into recognizing my mapped queries as valid PrismaPromise objects:

export class MappedQuery<T, R> implements PrismaPromise<R> {
  private query: Promise<R>;

  public [Symbol.toStringTag]: "PrismaPromise" = "PrismaPromise";

  constructor(query: PrismaPromise<T>, mapper: (result: T) => R) {
    this.query = query.then(mapper);
  }

  then<TResult1 = R, TResult2 = never>(
    onfulfilled?: ((value: R) => TResult1 | PromiseLike<TResult1>) | null,
    onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
  ): Promise<TResult1 | TResult2> {
    return this.query.then(onfulfilled).catch(onrejected);
  }

  catch<TResult = never>(
    onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
  ): Promise<R | TResult> {
    return this.query.catch(onrejected) as Promise<R | TResult>;
  }

  finally(onfinally?: (() => void) | null | undefined): Promise<R> {
    return this.query.finally(onfinally) as unknown as Promise<R>;
  }
}

I can use this class like so:

export function getLatestArticles(date = new Date()) {
  return new MappedQuery(
    prismaClient.articles.findMany({
      orderBy: { date: "desc" },
      take: 20,
      where: { date: { gt: date } },
    }),
    (articles) => articles.map(articlesMapper)
  );
}

This works, but it feels unnecessarily verbose and requires some types juggling.

So attempted to simplify the implementation by extending directly the Promise class:

export class MappedQuery<T, R> extends Promise<R> implements PrismaPromise<R> {
  public [Symbol.toStringTag]: "PrismaPromise" = "PrismaPromise";

  constructor(query: Promise<T>, mapper: (result: T) => R) {
    super((resolve, reject) =>
      query.then((result) => resolve(mapper(result))).catch(() => reject())
    );
  }
}

However, this leads to an error stating query.then is not a function. The constructor seems to be called twice: once with a query parameter of type Promise and another time with the query parameter receiving a plain function (returning undefined).

Question: Why does the MappedQuery constructor get called twice, and how can I correctly implement a mapped promise wrapper that works with Prisma's $transaction?


Solution

  • You can consider using the undocumented method on the prisma client called _createPrismaPromise which takes one argument, a function to wrap, and returns a PrismaPromise, which can then be used in prisma transactions.