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?
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.