Skip to content

Zenstack is using base Prisma queries instead of extended ones, when overriding queries during Prisma.Extensions #1173

Closed
@CollinKempkes

Description

@CollinKempkes

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

import { Prisma } from '@prisma/client';
import {
  postTransformations,
  preTransformations,
} from '@vdrip-database/database.util-test';

export const CurrencyTransformationExtensionTest = Prisma.defineExtension({
  name: 'Currency Transformation',

  query: {
    $allModels: {
      async $allOperations({ operation, args, query }) {
        const valueFields = ['data', 'create', 'update', 'where'];

        switch (operation) {
          // 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': {
            valueFields.forEach((field) => {
              // @ts-expect-error
              if (args[field]) {
                // @ts-expect-error
                args[field] = preTransformations.args.currencyTransform(
                  // @ts-expect-error
                  args[field]
                );
              }
            });
          }
        }

        return postTransformations.result.currencyTransform(await query(args));
      },
    },
  },
});

Override of the currency field is happening here:
preTransformations.args.currencyTransform(...)

Here is the impl of that:
database.util.ts

import { Prisma } from '@prisma/client';
import {
  convertCurrencyToVdripCurrency as convertCurrencyToInternalCurrency,
  convertVdripCurrencyToCurrency,
} from '@vdrip-app/stripe/payment.utils';
import { containsAttribute } from '@vdrip-app/utils/objects';

export const doesModelIncludeField = (model: string, fieldName: string) => {
  const dbModels = Prisma.dmmf.datamodel.models;
  const dbModel = dbModels.find((dbModel) => dbModel.name === model);
  return dbModel?.fields.some((field) => field.name === fieldName);
};

export const preTransformations = {
  args: {
    currencyTransform: (data: any) => {
      const currencyFields = ['currency'];

      currencyFields.forEach((field) => {
        if (data && data[field]) {
          data[field] = convertCurrencyToInternalCurrency(data[field]);
        }
      });

      return data;
    },
    softDeleteTransform: (model: string, column: string, where: any) => {
      const isDeleteable = doesModelIncludeField(model, column);

      if (isDeleteable && !containsAttribute(column, where)) {
        where = { ...where, deleted_at: null };
      }

      return where;
    },
  },
};

export const postTransformations = {
  result: {
    currencyTransform: (result: any) => {
      if (!result) {
        return result;
      }

      const currencyFields = ['currency'];

      currencyFields.forEach((field) => {
        if (Array.isArray(result)) {
          result = result.map((result) => {
            if (result[field]) {
              result[field] = convertVdripCurrencyToCurrency(result[field]);
            }
            return result;
          });
        }

        if (result[field]) {
          result[field] = convertVdripCurrencyToCurrency(result[field]);
        }
      });

      return result;
    },
  },
};

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

it('should transform currencies automatically, but save them differently', async () => {
        // setup
        const extendedAndEnhancedPrisma = enhance(testContext.prismaClient);
        const unextendedEnhancedPrisma = enhance(unextendedPrismaClient);

        const testEntity = await testContext.prismaClient.test.create({
          data: {
            currency: 'USD',
          },
        });
        testEntity;
        //! ##### The Code is working until here #####

        const enhancedTestEntity = await extendedAndEnhancedPrisma.test.create({
          data: {
            currency: 'USD',
          },
        });

        // effects
        const transformedEntity =
          await extendedAndEnhancedPrisma.test.findFirst({
            where: {
              id: enhancedTestEntity.id,
            },
          });
        const realEntity = await unextendedEnhancedPrisma.test.findFirst({
          where: {
            id: enhancedTestEntity.id,
          },
        });

        // post-conditions
        expect(transformedEntity).toEqual(
          expect.objectContaining({
            currency: enhancedTestEntity.currency,
          })
        );
        expect(realEntity).toEqual(
          expect.objectContaining({
            currency: convertCurrencyToVdripCurrency(
              enhancedTestEntity.currency as AcceptedPresentmentCurrencies
            ),
          })
        );
      });

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 is USD).

For completeness, here the zmodel:
test.zmodel

model Test {
  id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
  currency String

  @@validate(currency in [
      'ZZEUR',
      'ZZUSD'
    ],
    'type must be one of the following values: ZZEUR, ZZUSD'
  )

  @@schema("public")
  @@map("test")

  @@allow("all", true)
}

The error that is occurring inside extendedAndEnhancedPrisma.test.create(...) is

Error calling enhanced Prisma method `create`: denied by policy: test entities failed 'create' check, input failed validation: Validation error: type must be one of the following values: ZZEUR, ZZUSD

Environment:

  • ZenStack version: 1.11.1
  • Prisma version: 5.9.1
  • Database type: Postgresql

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions