Prisma客户扩展:用例和陷阱
#网络开发人员 #typescript #database #prisma

尽管仍在实验中,但是Client Extensions是最近Prisma版本中介绍的最令人兴奋的功能之一。为什么?因为它为开发人员打开了一扇门,以极大的灵活性将自定义行为注入PrismaClient。这篇文章显示了此功能启用了一些有趣的方案,以及关于我们应该在哪里设置边界以避免过度使用其力量的想法。

背景

在引入客户端扩展之前,middleware是扩展Prisma的核心运行时功能的唯一方法 - 您可以使用它来更改查询参数并更改结果。客户扩展是作为将来更换中间软件的替代品创建的,并具有更大的灵活性和类型的安全性。这是您可以处理的快速列表:

  • 为模型添加自定义方法
const xprisma = prisma.$extends({
  model: {
    user: {
      async signUp(email: string) {
        return prisma.user.create({ data: { email } });
      },
    },
  },
});

const user = await xprisma.user.signUp('john@prisma.io');
  • 向客户端添加自定义方法
const xprisma = prisma.$extends({
  client: {
    $log: (s: string) => console.log(s),
  },
});

prisma.$log('Hello world');
  • 在查询结果中添加自定义字段
const xprisma = prisma.$extends({
  result: {
    user: {
      fullName: {
        // the dependencies
        needs: { firstName: true, lastName: true },
        compute(user) {
          // the computation logic
          return `${user.firstName} ${user.lastName}`;
        },
      },
    },
  },
});

const user = await xprisma.user.findFirst();
console.log(user.fullName);
  • 自定义查询行为
const xprisma = prisma.$extends({
  query: {
    user: {
      async findMany({ model, operation, args, query }) {
        // inject an extra "age" filter
        args.where = { age: { gt: 18 }, ...args.where };
        return query(args);
      },
    },
  },
});

await xprisma.user.findMany(); // returns users whose age is greater than 18

用例

客户扩展非常适合解决跨切割问题。这里有一些用例来刺激您的灵感。

1.软删除

软删除是通过在实体上放置标记而无需真正删除它们来处理删除的一种流行方式,以便在必要时可以快速恢复数据。这是如此广泛地希望,在Prisma的github上有一个持久的问题-Soft deletes (e.g. deleted_at) #3398

使用客户端扩展,您可以在中心位置实现软删除。例如,假设您有这样的架构:

model User {
  id      Int     @id @default(autoincrement())
  email   String  @unique
  name    String?
  posts   Post[]
  deleted Boolean @default(false)
}

model Post {
  id       Int     @id @default(autoincrement())
  title    String
  content  String?
  author   User?   @relation(fields: [authorId], references: [id])
  authorId Int?
  deleted  Boolean @default(false)
}

软删除可以如下:

const xprisma = prisma.$extends({
  name: 'soft-delete',
  query: {
    $allModels: {
      async findMany({ args, query }) {
        // inject read filter
        args.where = { deleted: false, ...args };
        return query(args);
      },

      // ... other query methods like findUnique, etc.

      async delete({ model, args }) {
        // translate "delete" to "update"
        return (prisma as any)[model].update({
          ...args,
          data: { deleted: true },
        });
      },

      // ... deleteMany
    },
  },
});

使用xprisma客户端进行的所有查询和突变现在都具有软删除行为。通过客户端扩展实施软删除的好处是,由于客户端扩展不改变原始Prisma客户端的行为,因此您仍然可以使用原始客户端获取所有实体,包括标记为已删除的实体。

好奇的读者可能会发现样本实现不完整。请继续阅读;我们将在陷阱部分覆盖更多。

2.限制结果批次大小

prisma s findMany方法默认返回所有记录,这对于具有许多行的表来说可能是不必要的行为。我们可以使用客户扩展名来添加安全保护人员:

const MAX_ROWS = 100;
const xprisma = prisma.$extends({
  name: 'max-rows',
  query: {
    $allModels: {
      async findMany({ args, query }) {
        return query({ ...args, take: args.take || MAX_ROWS });
      },
    },
  },
});

3.记录

记录是另一个非常普遍的跨切割问题。有时您想记录某些重要的CRUD操作,但是打开PrismaClient的完整登录可能会令人沮丧。现在,通过客户扩展很容易实现。

const xprisma = prisma.$extends({
  name: 'logging',
  query: {
    post: {
      async delete({ args, query }) {
        const found = await prisma.post.findUnique({
          select: { title: true, published: true },
          where: args.where,
        });
        if (found && found.published) {
          myLogger.warn(`Deleting published post: ${found.title}`);
        }
        return query(args);
      },
    },
  },
});

4.制定访问控制规则

大多数数据库驱动的应用程序都有用于访问控制的业务规则,必须在多个功能领域始终执行。传统上,这种做法是在API层实施它们,但易于不一致。 Prisma客户端扩展现在提供了更靠近数据库的可能性。

假设您使用express.js实现API;您可以这样做:

function getAuthorizedDb(prisma: PrismaClient, userId: number) {
  return prisma.$extends({
    name: 'authorize',
    query: {
      post: {
        async findMany({ args, query }) {
          return query({ ...args, where: { authorId: userId } });
        },
        // ... other operations
      },
    },
  });
}

app.get('/posts', (req, res) => {
  const userId = req.userId; // provided by some authentication middleware
  return getPosts(getAuthorizedDb(userId));
});

客户扩展的美在于,他们与原始的Prisma客户端共享相同的查询引擎和连接池,因此创建它们的成本非常低,您可以以Per的方式完成 - 重新测量级别,如上所述。

局限性和陷阱

客户扩展仍然是相当新的,而且它们并非没有限制和陷阱。这是您可能需要注意的一些重要的:

1.强大的打字总是不起作用

Prisma在确保事情总是很好地打字方面做得很好。即使对于客户扩展,一个重要的设计目标是在实施扩展时支持强型编程。但是,正如您在软删除中看到的那样,目前尚不一致地可以实现。

2.倾向于与他们实施业务逻辑

客户端扩展使您可以在模型或整个客户端中添加任意方法。它可以使您诱人地使用它实施业务逻辑。例如,您可能需要在用户模型中添加signUp方法,除了创建实体外,还要发送激活电子邮件。它将起作用,但是您的业务代码开始渗入数据库领域,使代码底座更难理解和故障排除。

但是,如前所述,跨切割问题,例如软删除,日志记录,访问控制等是非常有效的用例。

3.注射过滤器条件可能非常棘手

正如您在用例1和4中看到的那样,我们将额外的条件注入Prisma查询中,以实现其他过滤。不幸的是,这两个都不是正确的。 Prisma的查询API对于获取关系非常灵活。因此,对于“软删除”示例,除了处理顶级findMany外,我们还需要处理关系提取,例如:

prisma.user.findMany({ include: { posts: true } });

// should be injected as
prisma.user.findMany({
  where: { deleted: false } },
  include: { posts: { where: { deleted: false } }
});

,如果您具有深厚的关系层次结构,则需要递归处理。注意突变方法,例如updatedelete遇到了相同的问题,因为它们的结果也可以通过使用“ Include”子句携带关系数据。我们使用的示例是A * - 到许多方案。一对一的关系甚至更难处理,因为您可以在关系的一侧找到过滤器。实现非常容易。

所有这些复杂性都促使我们创建了ZenStack工具包,以系统地增强Prisma,并允许您声明地对访问控制问题进行建模。该工具包在运行时进行繁重的举重,以确保对查询进行正确的过滤并进行防护,以免自己处理所有的微妙之处。