-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Cannot type extended client in function arguments #20326
Comments
Thanks for the detailed issue and summarizing our conversation with all the needed references. I don't expect this to be a high priority in the short term, but this is an improvement that we will be able to bring in the longer term, and just to re-state it here:
My tl;dr; is that type assignability rules prevent you to assign an extended client to a non-extended client and vice-versa. So the workaround is to cast either the one or the other to a common PrismaClient type. |
This might be better to make as a separate issue but related: The Transaction type created by an extended client is not the same as the transaction type created by a standard prisma client, so also cannot pass the extended transaction to a standalone function using the Prisma.TransactionClient that's already exposed. |
Running into the same issue in our project, we pass the |
@jo-warren We have two issues about that, so leave your 👍 on these please: |
Error: Property '$on' does not exist on type 'DynamicClientExtensionThis<TypeMap<InternalArgs & { result: {}; model: {}; query: {}; client: { $begin: () => () => Promise; }; }>, TypeMapCb, { ...; }>'.ts(2339) we're basically using prisma client extension to get control over transaction with commit and rollback. Until the log was configured public readonly prisma = new PrismaClient({ log: process.env.NODE_ENV === SharedTypes.NODE_ENV.PRODUCTION ? ["error"] : ["query", "info", "warn", "error"] }).$extends({ import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
import { Prisma, PrismaClient } from "@prisma/client";
import { DynamicClientExtensionThis, InternalArgs } from "@prisma/client/runtime/library";
export type PrismaFlatTransactionClient = Prisma.TransactionClient & {
$commit: () => Promise<void>;
$rollback: () => Promise<void>;
};
export type ExtendedPrismaClient = DynamicClientExtensionThis<
Prisma.TypeMap<
InternalArgs & {
result: object;
model: object;
query: object;
client: {
$begin: () => () => Promise<PrismaFlatTransactionClient>;
};
}
>,
Prisma.TypeMapCb,
{
result: object;
model: object;
query: object;
client: {
$begin: () => () => Promise<PrismaFlatTransactionClient>;
};
}
>;
@Injectable()
export class PrismaService extends PrismaClient<Prisma.PrismaClientOptions, "query" | "info" | "warn" | "error"> implements OnModuleInit {
public readonly logger: Logger;
readonly rollback = {
[Symbol.for("prisma.client.extension.rollback")]: true,
};
constructor() {
super();
this.logger = new Logger("PrismaService");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.prisma.$on("query", (event: any) => {
this.logger.debug(`Query - [${event.query}}] \n with Parameters ${event.params}} \n time-taken ${event.duration}ms \n`);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.prisma.$on("error", (event: any) => {
this.logger.error(`Prisma ERROR :- \n ${JSON.stringify(event, null, 2)} \n `);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.prisma.$on("info", (event: any) => {
this.logger.log(`Prisma INFO :- ${JSON.stringify(event, null, 2)} \n `);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.prisma.$on("warn", (event: any) => {
this.logger.warn(`Prisma WARNING :- ${JSON.stringify(event, null, 2)} \n`);
});
}
public readonly prisma = new PrismaClient({
log: [
{
emit: "event",
level: "query",
},
{
emit: "stdout",
level: "error",
},
{
emit: "stdout",
level: "info",
},
{
emit: "stdout",
level: "warn",
},
],
}).$extends({
client: (() => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this; // Capture the `this` value from the outer scope
return {
async $begin() {
const prisma = Prisma.getExtensionContext(this);
let setTxClient: (txClient: Prisma.TransactionClient) => void;
let commit: () => void;
let rollback: () => void;
// a promise for getting the tx inner client
const txClient = new Promise<Prisma.TransactionClient>((res) => {
// eslint-disable-next-line jsdoc/require-returns
/**
* Sets the transaction client.
* @param txClient - The transaction client.
*/
// eslint-disable-next-line @typescript-eslint/no-shadow
setTxClient = (txClient) => res(txClient);
});
// a promise for controlling the transaction
const txPromise = new Promise((res, rej) => {
/**
* Commits the transaction.
* @returns - A promise that resolves when the transaction is committed.
*/
commit = () => {
return res(undefined);
};
/**
* Rolls back the transaction.
* @returns - A promise that rejects with the rollback symbol.
*/
rollback = () => {
return rej(self.rollback);
};
});
// opening a transaction to control externally
if ("$transaction" in prisma && typeof prisma.$transaction === "function") {
const tx = prisma
.$transaction((newTxClient) => {
// Should be avoided, but we need to set the txClient
const localClient = newTxClient as Prisma.TransactionClient;
setTxClient(localClient);
return txPromise;
})
.catch((e) => {
if (e === self.rollback) return;
throw e;
});
// return a proxy TransactionClient with `$commit` and `$rollback` methods
return new Proxy(await txClient, {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(target, prop: any) {
if (prop === "$commit") {
return () => {
commit();
return tx;
};
}
if (prop === "$rollback") {
return () => {
rollback();
return tx;
};
}
return target[prop];
// return target[prop as keyof typeof target];
},
}) as PrismaFlatTransactionClient;
}
throw new Error("Transactions are not supported by this client");
},
};
})(),
});
async onModuleInit() {
await this.connect();
}
} |
Bug description
Some context
I maintain a middleware that provides field-level encryption, which I'm porting to the client extension mechanism (PR 47ng/prisma-field-encryption#66).
It can also be used as a generator to generate data migration files to rotate encryption keys. Those generated files look like the following: a function that takes a Prisma client as an argument to perform the work.
The reason why it's accepting a client rather than creating one itself is because of configuration of the middleware/extension, and to potentially include other middleware and extensions. The idea is to make the data migration process as transparent as possible: reading & updating records in an iterative manner, just as it would be done in application code.
The problem
The issue I encounter is with defining the type of client and connecting it to extended client type definitions. I'd like to maintain both a middleware and extension API for this library, for retrocompatibility, until Prisma drops middleware support entirely.
In integration tests, it seems like the only way to have a client type be either middleware-based or extension-based is to explicitly cast the extended client
as PrismaClient
, see 47ng/prisma-field-encryption#63 (comment).There are also some discrepancies between using the PrismaClient type imported from
@prisma/client
vs.custom/client/location
. The former doesn't seem to support interactive transaction types (the client argument of the transaction callback is typed asany
), but the latter seems to fit the bill for all model operations, which is what we're using to allow supporting custom client locations anyway.That being said, this PrismaClient type is not type-compatible with extended clients. Runtime compatibility is fine, all model operations work if using directives like explicit type casts or
// @ts-ignore
.TL;DR: There doesn't seem to be a way at the moment to type extended clients as arguments of standalone functions.
cc @millsp
How to reproduce
Reproduction repository: https://github.com/franky47/prisma-field-encryption-sandbox
Steps to reproduce:
yarn install
prisma generate
migrate.ts
file, and remove theas PrismaClient
type castExpected behavior
There should be a type definition for extended clients that allows them to be passed to external functions.
While the extended API can include anything added by extensions, having at least the ability to get a working type for the basic functionality (model operations, but not allowing
$use
or$on
) would already be a great start. Those types could then be augmented by users if need be at specific call site locations with added methods from extensions.Prisma information
Environment & setup
Prisma Version
The text was updated successfully, but these errors were encountered: