Skip to content
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

Static type checking takes forever #23761

Open
Yoran-impactID opened this issue Apr 8, 2024 · 0 comments
Open

Static type checking takes forever #23761

Yoran-impactID opened this issue Apr 8, 2024 · 0 comments
Labels
bug/1-unconfirmed Bug should have enough information for reproduction, but confirmation has not happened yet. domain/client Issue in the "Client" domain: Prisma Client, Prisma Studio etc. kind/bug A reported bug. tech/typescript Issue for tech TypeScript. topic: client types Types in Prisma Client topic: clientExtensions topic: type performance

Comments

@Yoran-impactID
Copy link

Bug description

Since updating from Prisma 4 to 5 static type checking takes forever. We are using NextJS 14, when building it takes extremely long. We use a lot of Prisma extensions and transactions.

After doing some of our own research, might it be related to this issue?

How to reproduce

  1. Use our schema
  2. Use our Prisma extensions
  3. Create a transaction for every database write.
  4. Build NextJS

Expected behavior

That type checking doesn't take longer than with Prisma 4.

Prisma information

Schema: (Too long for Github Isssue)
https://pastebin.com/4gVW6Rhp

Prisma Extensions:

//Declare a global prisma client to avoid errors with NextJS' hot refresh
declare global {
  var prismaClient: PrismaClient | undefined;
}

//Instantiate a new Prisma Client or load the global client
const prismaClient = global.prismaClient || new PrismaClient();
if (process.env.NODE_ENV !== "production") global.prismaClient = prismaClient;

//Models with a 'deleted' field to be extended
const extendedModels: string[] = [];
//Iterate over the Prisma models and add all models with a 'deleted' field to the array
for (const model of Prisma.dmmf.datamodel.models) {
  if (model.fields.find((field) => field.name == "deleted"))
    extendedModels.push(model.name);
}

//Recursive method to soft delete entities and any children with a required relation
//Already soft deleted entities will be undeleted by this method
const softDeleteEntities = async (
  prismaClient: PrismaClient,
  model: string,
  id: string
) => {
  //Check if the entity is deleted
  const { deleted } = await (prismaClient as any)[model].findUnique({
    where: { id },
  });
  //Perform the soft (un)delete based on the deleted flag in the entity
  await (prismaClient as any)[model].update({
    where: { id },
    data: {
      deleted: !deleted ? new Date() : null,
    },
  });
  //Get the schema data model to check if there are children relations to this entity to soft delete as well
  const dataModel = Prisma.dmmf.datamodel.models.find(
    (dataModel) => dataModel.name == model
  );
  if (dataModel) {
    const fields = dataModel.fields;
    //Iterate over the fields in the data model to check if there are nested fields that should also be soft deleted
    for (const field of fields) {
      //Check if there is a relation that is part of the extended models array
      if (extendedModels.includes(field.type)) {
        const nestedDataModel = Prisma.dmmf.datamodel.models.find(
          (dataModel) => dataModel.name == field.type
        );
        if (nestedDataModel) {
          const nestedField = nestedDataModel.fields.find(
            (nestedField) => nestedField.type == model
          );
          //Check the data model for the nested field to check if the relation is required. Otherwise it should not be soft deleted
          if (nestedField && nestedField.isRequired && !nestedField.isList) {
            //Get the nested entities
            const result = await (prismaClient as any)[model].findUnique({
              where: { id },
              include: {
                [field.name]: true,
              },
            });
            if (result) {
              if (result[field.name]?.id) //1:1 relation
                await softDeleteEntities(prismaClient, field.type, result[field.name].id);
              else if (Array.isArray(result[field.name])) {
                //1:N relation. Iterate over the result and recursively soft delete the children entities
                for (const nestedEntity of result[field.name]) {
                  if (nestedEntity.id)
                    await softDeleteEntities(
                      prismaClient,
                      field.type,
                      nestedEntity.id
                    );
                }
              }
            }
          }
        }
      }
    }
  }
};

//Recursive method to format a Prisma find argument block to only find non-deleted items
//Alters the where statement at the start of every recursion, and any nested include/select statement
const extendFindArgs = (args: any, model: string) => {
  const newArgs = { ...args };

  //Get the schema data model
  const dataModel = Prisma.dmmf.datamodel.models.find(
    (dataModel) => dataModel.name == model
  );
  if (dataModel) {
    const fields = dataModel.fields;
    //Check if this model is in the extended models list to alter any where statement to exclude soft deleted items
    if (extendedModels.includes(model))
      newArgs.where = {
        deleted: null,
        ...newArgs.where,
      };

    //Find an include statement in the arguments
    if (newArgs.include) {
      //Iterate over the entries in the include statement to check if there are relations with a soft delete field
      for (const includeField in newArgs.include) {
        const key = includeField as string;
        const value = newArgs.include[key] as boolean | {};
        const field = fields.find((field) => field.name == key);
        //Process when the type of this field is in the extended models list
        if (field && field.isList && extendedModels.includes(field.type)) {
          if (value === true)
            //Convert the boolean value to an object with where statement to exclude soft deleted items
            newArgs.include[key] = { where: { deleted: null } };
          //Recursively convert the include statement when an object is given
          else newArgs.include[key] = extendFindArgs(value, field.type);
        }
      }
    }

    //Find a select statement in the arguments
    if (args.select) {
      //Iterate over the entries in the select statement to check if there are relations with a soft delete field
      for (const selectField in newArgs.select) {
        const key = selectField as string;
        const value = newArgs.select[key] as boolean | {};
        const field = fields.find((field) => field.name == key);
        //Process when the type of this field is in the extended models list
        if (field && field.isList && extendedModels.includes(field.type)) {
          if (value === true)
            //Convert the boolean value to an object with where statement to exclude soft deleted items
            newArgs.select[key] = { where: { deleted: null } };
          //Recursively convert the select statement when an object is given
          else newArgs.select[key] = extendFindArgs(value, field.type);
        }
      }
    }
  }

  return newArgs;
};

//Define an extended Prisma Client to include overwrites of standard Prisma methods
const prisma = prismaClient.$extends({
  query: {
    $allModels: {
      //Extend a delete statement for models that should be soft deleted
      //The delete method will be rewritten to an update method
      async delete({ model, operation, args, query }) {
        if (extendedModels.includes(model)) {
          //Get the entity to be deleted to retrieve the primary key
          const result = await (prismaClient as any)[model].findUnique(args);
          if (result) await softDeleteEntities(prismaClient, model, result.id);
          //Return the result of the queries performed in softDeleteEntities
          return (prismaClient as any)[model].findUnique(args);
        }
        return query(args);
      },
      //Extend a deleteMany statement for models that should be soft deleted
      //The deleteMany method will be rewritten to an update method for every entity
      async deleteMany({ model, operation, args, query }) {
        if (extendedModels.includes(model)) {
          //Get the entities to be deleted to retrieve the primary key
          const result = await (prismaClient as any)[model].findMany(args);
          if (result) {
            for (const entity of result)
              await softDeleteEntities(prismaClient, model, entity.id);
          }
          return (prismaClient as any)[model].findMany(args);
        }
        return query(args);
      },
      //Extend a findUnique statement for models that can be soft deleted
      //The findUnique method will be rewritten to a findFirst with an extra where clause for the deleted flag
      async findUnique({ model, operation, args, query }) {
        if (extendedModels.includes(model))
          return (prismaClient as any)[model].findFirst(
            extendFindArgs(args, model)
          );

        return query(args);
      },
      //Extend a findUniqueOrThrow statement for models that can be soft deleted
      //The findUniqueOrThrow method will be rewritten to a findFirst with an extra where clause for the deleted flag
      async findUniqueOrThrow({ model, operation, args, query }) {
        if (extendedModels.includes(model))
          return (prismaClient as any)[model].findFirstOrThrow(
            extendFindArgs(args, model)
          );

        return query(args);
      },
      //Extend a findFirst statement for models that can be soft deleted
      //The findFirst method will be rewritten to a findFirst with an extra where clause for the deleted flag
      async findFirst({ model, operation, args, query }) {
        if (extendedModels.includes(model))
          return (prismaClient as any)[model].findFirst(
            extendFindArgs(args, model)
          );

        return query(args);
      },
      //Extend a findFirstOrThrow statement for models that can be soft deleted
      //The findFirstOrThrow method will be rewritten to a findFirst with an extra where clause for the deleted flag
      async findFirstOrThrow({ model, operation, args, query }) {
        if (extendedModels.includes(model))
          return (prismaClient as any)[model].findFirstOrThrow(
            extendFindArgs(args, model)
          );

        return query(args);
      },
      //Extend a findMany statement for models that can be soft deleted
      //The findMany method will be rewritten to a findMany with an extra where clause for the deleted flag
      async findMany({ model, operation, args, query }) {
        if (extendedModels.includes(model))
          return (prismaClient as any)[model].findMany(
            extendFindArgs(args, model)
          );

        return query(args);
      },
    },
  },
}) as PrismaClient;

Example DB write

const writeActivityRegistration = async (prisma: Prisma.TransactionClient, data: ActivityRegistrationInput) => {
  //If the duration is set, make sure that the decimals are minutes (0-59)
  let duration = data.duration;
  if (duration) {
    const durationDuration = getDurationFromNumber(Number(duration));
    duration = formatTimeFromDuration(durationDuration).replace(":", ".");
  }
  let activityRegistration: ActivityRegistration;
  if (
    await prisma.activityRegistration.findUnique({
      where: { id: data.id || "" },
    })
  ) {
    //Omit the contributor when updating an existing activity registration
    const { contributorId, contributor, ...activityRegistrationData } = data;
    activityRegistration = await prisma.activityRegistration.update({
      where: { id: data.id },
      data: { ...activityRegistrationData, duration }
    })
  } else {
    //Omit the contributor when updating an existing activity registration
    const { contributor, ...activityRegistrationData } = data;
    activityRegistration = await prisma.activityRegistration.create({
      data: { ...activityRegistrationData, duration }
    });
  }

  return activityRegistration.id;
};

    //Start the transaction
    const activityRegistrationId = await prisma.$transaction(async (prisma) => {
      return await writeActivityRegistration(prisma, data);
    });

Environment & setup

  • OS: Windows 10/11 & WSL (Ubuntu 20.04)
  • Database: MySQL
  • Node.js version: 20.11.0

Prisma Version

5.12.0
@Yoran-impactID Yoran-impactID added the kind/bug A reported bug. label Apr 8, 2024
@laplab laplab added tech/typescript Issue for tech TypeScript. domain/client Issue in the "Client" domain: Prisma Client, Prisma Studio etc. bug/1-unconfirmed Bug should have enough information for reproduction, but confirmation has not happened yet. topic: client types Types in Prisma Client topic: type performance labels Apr 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug/1-unconfirmed Bug should have enough information for reproduction, but confirmation has not happened yet. domain/client Issue in the "Client" domain: Prisma Client, Prisma Studio etc. kind/bug A reported bug. tech/typescript Issue for tech TypeScript. topic: client types Types in Prisma Client topic: clientExtensions topic: type performance
Projects
None yet
Development

No branches or pull requests

3 participants