Search code examples
typescriptormprisma

Prisma upsert and delete old relations


I have a project that is composed on Posts and each post can have a collection of Tags. I'm trying to enable an edit feature that allows Post tags to be updates / added / removed. Currently, my query upserts these tags, but I'm experiencing some trouble caused by the deletion aspect. Any time new tags are created, since they have no ID at the time, they are deleted immediately.

await Prisma.post.update({
            where: {
                id: postId,
            },
            data: {
                title,
                content,
                updatedAt: new Date(),
                tags: {
                    // Upsert tags, remove tags that are not in the request
                    upsert: tags.map((tag) => ({
                        where: {
                            id: tag.id ? tag.id : "0",
                        },
                        create: {
                            name: tag.name,
                            color: tag.color,
                            creator: {
                                connect: {
                                    id: session.user.id,
                                },
                            },
                        },
                        update: {
                            name: tag.name,
                            color: tag.color,
                        },
                    })),
                    deleteMany: {
                        id: {
                            notIn: tags.map((tag) => tag.id ?? "0"),
                        },
                    },
                },
            },
            include: {
                tags: true,
            },
        });

I've attempted various solutions such as keying on the name or other properties (e.g. name AND color). None of these seem to fix the issue, as they end up duplicating tags and causing similar bugs.

Here are the schema I'm using my my Post and Tag models.

Post

model Post {
    id        String    @id @default(uuid())
    createdAt DateTime  @default(now())
    updatedAt DateTime  @updatedAt
    deletedAt DateTime?

    title    String
    content  String
    authorId String
    author   User      @relation(fields: [authorId], references: [id])
    comments Comment[]
    tags     Tag[]
}

Tag

model Tag {
    id        String   @id @default(uuid())
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt

    name      String
    color     String
    creatorId String
    creator   User   @relation(fields: [creatorId], references: [id])
    posts     Post[]
}

If someone could point me in the right direction as to how one handles a situation like this, I would greatly appreciate it.


Solution

  • You should cut out your steps in a transaction to delete the tags not in the query before inserting the new ones. Then you can create and update your data and return your post

    With interactiveTransactions

    In your schema.prisma you should enable interactiveTransactions, for this:

    generator client {
      provider        = "prisma-client-js"
      previewFeatures = ["interactiveTransactions"]
    }
    

    Then cut out your steps in a transaction

    // Remove <Post> if you aren't in typescript
    await prisma.$transaction<Post>(async (trx) => {
      const existingTags = tags.filter(({ id }) => id);
      const newTags = tags.filter(({ id }) => !id);
    
      await trx.tag.deleteMany({
        where: {
          id: {
            notIn: existingTags.map(({ id }) => id),
          },
        },
      });
      // Update the existing tags
      await Promise.all(
        tags.map((tag) =>
          trx.tag.update({
            where: {
              id: tag.id,
            },
            data: {
              color: tag.color,
              name: tag.name,
            },
          })
        )
      );
      await trx.tag.createMany({
        data: newTags.map((tag) => ({
          ...tag,
          creator: {
            connect: {
              id: session.user.id,
            },
          },
        })),
      });
      return trx.post.update({
        where: {
          id: postId,
        },
        data: {
          title,
          content,
          updatedAt: new Date(),
        },
        include: {
          tags: true,
        },
      });
    });