-
-
Notifications
You must be signed in to change notification settings - Fork 107
Zenstack is using base Prisma queries instead of extended ones, when overriding queries during Prisma.Extensions #1173
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
Comments
Addition: ...
const testEntity =
await testContext.prismaClient.test.createWithCurrency({
data: {
currency: 'USD',
},
});
testEntity;
const enhancedTestEntity =
await extendedAndEnhancedPrisma.test.createWithCurrency({
data: {
currency: 'USD',
},
});
...
...
model: {
$allModels: {
async createWithCurrency<Model, Args extends object>(
this: Model,
args: Prisma.Exact<Args, Prisma.Args<Model, 'create'>>
): Promise<Prisma.Result<Model, Args, 'create'>> {
// @ts-expect-error
args.data = preTransformations.args.currencyTransform(
// @ts-expect-error
args.data
);
// @ts-expect-error: Requires more types from Prisma
return this.create({
...args,
});
},
},
},
... This test is running green now. But I would say the desired behavior is that it also works with query overrides instead of model query creations. |
Hi @CollinKempkes , thank you for filing the issue with great details! Could you try to enhance the client first and then install the extension? The reason is that both Prisma client extensions and ZenStack enhancements are Javascript proxies around the original prisma client. In your current setup, the client extension proxy directly wraps around the client, and then ZenStack wraps outside of it. So when you make a Extending with a new I understand it may be a bit confusing. Let me know if you feel more clarification is needed. |
Thanks for your response @ymc9, the explanation really makes sense, thx for it! Basically I ran this test to check if everything works fine: it('test', async () => {
// setup
const extendedUser =
await testContext.prismaClient.user.findFirstPermissionsAndRestrictions({
where: {
id: createdEntities.sellerUser.id,
},
});
const enhancedExtendedPrisma = new ExtendedPrismaService(
enhance(new PrismaClient(), {
user: extendedUser,
})
).withExtensions();
const extendedEnhancedPrisma = enhance(
new ExtendedPrismaService(new PrismaClient()).withExtensions(),
{
user: extendedUser,
}
);
const test = await enhancedExtendedPrisma.test.create({
data: {
currency: 'DDDDD',
},
});
test;
// ### The following query will break ###
const test2 = await extendedEnhancedPrisma.test.create({
data: {
currency: 'DDDDD',
},
});
test2;
});
the query for same thing for permissions:
Now the second query will fail with the error: Just to ensure that all information are given, here is the code of the extended-prisma-service: Expand me for ExtendedPrismaService...
export class ExtendedPrismaService {
constructor(
@Inject(PrismaClient) private readonly prismaClient: PrismaClient
) {}
withExtensions() {
return this.prismaClient
.$extends({
name: 'Extended Prisma Client',
model: {
$allModels: {
findFirstNotNull,
},
identity: {
findFirstWithRevokableListingsAndAsks,
},
localizedProperty: {
findFirstWithAllRelations,
},
user: {
findFirstPermissionsAndRestrictions,
},
},
})
.$extends(SoftDeleteExtension)
.$extends(CurrencyTransformationExtension);
}
} |
It seems when |
Hi @CollinKempkes , I debugged through some of Prisma's code and felt there isn't a good way to get it to work, when client extensions are installed onto an enhanced prisma client - at least for "query" extension. Internally Prisma calls into the original client and bypasses ZenStack's enhancement. My apologies for giving an incorrect suggestion initially. I think a possible (a bit more complex) solution is to turn the client extension into a regular JS proxy. Something like: const enhanced = enhance(prisma, undefined, { logPrismaQuery: true });
const extendedAndEnhancedPrisma = new Proxy(enhanced, {
get(target, prop, receiver) {
const allModels = ['test'];
if (!allModels.includes(prop as string)) {
// not a model field access
return Reflect.get(target, prop, receiver);
}
const value = Reflect.get(target, prop, receiver);
if (value && typeof value === 'object') {
// proxy the model-level prisma client
return new Proxy(value, {
get(target, prop, receiver) {
const valueFields = ['data', 'create', 'update', 'where'];
switch (prop) {
// For all select operations
case 'aggregate':
case 'count':
case 'findFirst':
case 'findFirstOrThrow':
case 'findMany':
case 'findUnique':
case 'groupBy':
case 'upsert':
case 'update':
case 'updateMany':
case 'findUniqueOrThrow':
// For all mutation operations
case 'create':
case 'createMany':
case 'update':
case 'updateMany':
case 'upsert': {
return async (args: any) => {
valueFields.forEach((field) => {
if (args[field]) {
args[field] = preTransformations.args.currencyTransform(args[field]);
}
});
const result = await Reflect.get(target, prop, receiver)(args);
return postTransformations.result.currencyTransform(result);
};
}
}
return Reflect.get(target, prop, receiver);
},
});
}
},
}); |
No worries and thanks for your new suggestion. It seems like it works, but it does not work with |
Thank you! Btw, I've added a dedicated documentation for client extensions here: https://zenstack.dev/docs/guides/client-extensions |
Thanks for adding the docs! :) Just a short update, it seems like it is working with this code here: import { Prisma } from '@prisma/client';
...
import { isNil } from 'ramda';
/**
* `withCurrencyFormat()` extends all queries of the prisma client to apply
* currency code tranformations in both persistence and retrieval.
*/
export const withCurrencyFormat = <Client extends object>(
client: Client
): Client => {
return new Proxy(client, {
get(target, prop, receiver) {
const reflected = Reflect.get(target, prop, receiver);
const allowedMethods = [ // TODO: Check if there is a better way than explicitly mentioning the allowed methods to change
'aggregate',
'count',
'findFirst',
'findFirstOrThrow',
'findMany',
'findUnique',
'groupBy',
'upsert',
'update',
'updateMany',
'findUniqueOrThrow',
'create',
'createMany',
'update',
'updateMany',
'upsert',
];
const isTransaction = prop === '$transaction';
/**
* Recursively proxy when we are running a transaction.
*/
if (isTransaction) {
return async (...args: any) => {
const [callback] = args;
return callback(withCurrencyFormat(client));
};
}
const [c1, ...model] = prop as string;
const isModel = Object.values(Prisma.ModelName).includes(
`${c1?.toUpperCase()}${model.join('')}` as Prisma.ModelName
);
/**
* Recursively proxy when we are running a model query.
*/
if (isModel) return withCurrencyFormat(reflected as Client);
if (!allowedMethods.includes(prop as string)) return reflected; // This is mandatory to not override Prisma internal calls
/**
* Proxy specific operations
*/
if (typeof reflected === 'function') {
const fields = new Set(['data', 'create', 'update', 'where']);
return async (args: any) => {
fields.forEach((field) => {
if (args?.[field]) {
args[field] = preTransformations.args.currencyTransform(
args[field]
);
}
});
// @ts-expect-error
const result = await reflected(args);
if (isNil(result)) return result;
return Array.isArray(result)
? result.map((data) =>
postTransformations.result.currencyTransform(data)
)
: postTransformations.result.currencyTransform(result);
};
}
return reflected;
},
});
}; The sequence ob building the client is like this:
On first tests it seemed to have problems with the newest version I will close this issue after I resolved all other open todos. Just wanted to add this code here, so you know that there is a way to make it work :) |
Alright it seems to work out now after adding some changes to the transaction adjustment. import { Prisma } from '@prisma/client';
import { DMMF } from '@prisma/generator-helper';
...
import { isNil } from 'ramda';
/**
* `withCurrencyFormat()` extends all queries of the prisma client to apply
* currency code tranformations in both persistence and retrieval.
*/
export const withCurrencyFormat = <Client extends object>(
client: Client
): Client => {
return new Proxy(client, {
get(target, prop, receiver) {
const reflected: any = Reflect.get(target, prop, receiver);
const baseModelQueries = Object.keys(DMMF.ModelAction);
const isTransaction = prop === '$transaction';
/**
* Use proxy for transactions callback.
*/
if (isTransaction) {
return (callback: any, ...rest: any[]) => {
if (typeof callback !== 'function') {
throw new Error('A function value input is expected');
}
return reflected.bind(target)((tx: any) => {
return callback(withCurrencyFormat(tx));
}, ...rest);
};
}
const [c1, ...model] = prop as string;
const isModel = Object.values(Prisma.ModelName).includes(
`${c1?.toUpperCase()}${model.join('')}` as Prisma.ModelName
);
/**
* Recursively proxy when we are running a model query.
*/
if (isModel) return withCurrencyFormat(reflected as Client);
if (!baseModelQueries.includes(prop as Prisma.DMMF.ModelAction))
return reflected;
/**
* Proxy specific operations
*/
if (typeof reflected === 'function') {
const fields = new Set(['data', 'create', 'update', 'where']);
return async (args: any) => {
fields.forEach((field) => {
if (args?.[field]) {
args[field] = preTransformations.args.currencyTransform(
args[field]
);
}
});
const result = await reflected(args);
if (isNil(result)) return result;
return postTransformations.result.currencyTransform(result);
};
}
return reflected;
},
});
}; Handling arrays vs objects vs attributes is done inside the transformer scripts |
Thanks for sharing it. Great to know it's working now! |
Uh oh!
There was an error while loading. Please reload this page.
Description and expected behavior
We are trying to integrate some internal mapping of currencies. Therefore we committed ourselves to a currency length of five, if some currencies are shorter they will be prepended by Z until they reach that amount of chars. We do this by overriding mutation queries inside prisma extensions. Additionally we want to use inside of the app the normal currencies (so we don't have to map all these custom currencies in the runtime, when we do API calls to service providers). So we have some kind of pre- and postTransformations of queries.
currency-extension.ts
Override of the currency field is happening here:
preTransformations.args.currencyTransform(...)
Here is the impl of that:
database.util.ts
Additionally we override the response got by prisma
return postTransformations.result.currencyTransform(await query(args));
. The problem seems to be that an enhanced + extended prisma client is using base methods of Prisma.Therefore this test scenario:
test.spec.ts
Everything really looks fine until the enhanced create. the preTransformation is working (in the db we have ZZUSD) saved and the postTransformation is working (when looking at
testEntity
it is saying that currency isUSD
).For completeness, here the zmodel:
test.zmodel
The error that is occurring inside
extendedAndEnhancedPrisma.test.create(...)
isEnvironment:
Additional context
Dit not try yet to use new model functions instead of query overrides, will try this now and will comment the outcome.
If you need more code example just hit me up :)
The transformer of the currency filling it pretty basic for now we can just say it adds static 'ZZ' to the currency when writing and does
.slice(2)
, when reading.The text was updated successfully, but these errors were encountered: