如何在Prisma中添加自定义属性
#javascript #网络开发人员 #生产率 #prisma

Prisma模式是直观的,可以让您以人为可读的方式声明数据库表。我最喜欢的事情是能够将其用作我应用程序数据模型的真实源。但是,为了获得更多情况,应该有一个良好的扩展机制。否则,Prisma本身必须涵盖所有不同的案件才能普遍实现。

扩展它的最直接方法是使用自定义属性。不幸的是,它尚未得到支持,似乎在可预见的将来。查看问题的悠久历史:

Allow custom field attributes #3102

假设我想确保,在创建电子邮件之前,电子邮件始终是有效的。 如果我能添加这样的@email指令,那就太棒了:

   model   user  { {
			在 @id   
			在在
			}  

虽然有道理,但现在不允许使用@email,如果我们允许的话,用户现在可以添加电子邮件验证中间件。 允许顶级@email可能没有意义,以保持验证相当严格,以便我们也可以帮助您进行错别字。

但是,可以从可以从X-或html属性开始的HTTP标头中的相同,该属性可以从data-开始,我们可以为自定义指令添加一个名称空间,例如@custom.X

这将允许Prisma的整个扩展世界,可以通过中间warre进行处理。

当然,这只是一个疯狂的主意,它也可能只是垃圾,也许我们不应该这样做。 因此,让我们讨论是否有意义:)

另外,与以下相关问题:

在以下内容中,我将向您展示添加自定义属性的两种方法:

  1. Prisma的评论解决方法(如果您已经知道,可以跳过此部分)
  2. Zenstack的自定义属性。

评论Prisma的解决方法

开源社区的美丽就是只要有必要,总会有一个解决方案。请参阅社区提供的解决方案:

model Post {
  id String @id @default(uuid()) /// @zod.uuid()
  /// @zod.max(255, { message: "The title must be shorter than 256 characters" })
  title String
  contents String /// @zod.max(10240)
}
model User {
    id String @id @default(cuid())
    /// @HideField()
    password String
    /// @HideField({ output: true, input: true })
    secret String
    /// @HideField({ match: '@(User|Comment)Create*Input' })
    createdAt DateTime @default(now())
}

他们都使用Triple Slash注释来实现技巧,因为这些注释将在架构文件的抽象语法树(AST)中显示为AST节点的描述。然后,他们可以通过自己的发电机访问它。我将简要介绍如何做。

如何访问发电机中的属性

在简而言之,运行prisma generate命令时将调用发电机的代码。

实施生成器

您需要做的就是实现generatorHandler函数,以创建您自己的新生成器,如下所示:

import { DMMF, generatorHandler, GeneratorOptions } from '@prisma/generator-helper';
import { getDMMF } from '@prisma/internals';

generatorHandler({
    onManifest: () => ({
        prettyName: 'My Generator',
    }),
    onGenerate: async (options: GeneratorOptions) => {
        const prismaClientDmmf = await getDMMF({
            datamodel: options.datamodel,
        });

        const models: DMMF.Model[] = prismaClientDmmf.datamodel.models;
        console.log(models);
    },
});

在模式中使用发电机

然后,您可以在定义的Prisma模式中使用它:

generator myGenerator {
  // provider is the command to run the generatorHandler function defined above
  provider = "node index.js"
  // the output folder for the generated code
  output   = "./generated"
}

运行发电机

之后,运行prisma,您应该看到我的发电机的输出

Prisma schema loaded from prisma/schema.prisma
{
  enums: [],
  models: [
    {
      name: 'User',
      dbName: null,
      fields: [Array],
      primaryKey: null,
      uniqueFields: [],
      uniqueIndexes: [],
      isGenerated: false
    },
    {
      name: 'Post',
      dbName: null,
      fields: [Array],
      primaryKey: null,
      uniqueFields: [],
      uniqueIndexes: [],
      isGenerated: false
    }
  ],
  types: []
}

✔ Generated Prisma Client (4.8.0 | library) to ./node_modules/@prisma/client in 76ms

✔ Generated My Generator to ./prisma/generated in 2.32s

访问属性

假设我们想拥有一些自定义属性:

  • 属性@@允许在模型级别控制访问策略。
  • @email @email进行电子邮件验证。
  • 属性@password和@omit在密码字段上加密并通过正常读取忽略它。
// everyone can signup, and user profile is also publicly readable
/// @@allow('create,read', true)
model User {
    id String @id()
    name String?
    /// @email
    email String? @unique()
    /// @password
    /// @omit
    password String?
}

三重注释将设置为AST节点的documentation属性,因此您可以如下访问它:

const userModel = datamodel.models.find((model) => model.name === 'User');
console.log(`user model attribute: ${userModel?.documentation}`);

const emailField = userModel?.fields.find((field) => field.name === 'email');
console.log(`email field attribute: ${emailField?.documentation}`);

const passwordField = userModel?.fields.find((field) => field.name === 'password');
console.log(`password field attribute: ${passwordField?.documentation}`);

运行后的输出是:

user model attribute: @@allow('create,read', true)
email field attribute: @email
password field attribute: @password
@omit

问题

尽管起作用,但使用注释有明显的缺陷:

  • 没有语法检查。这意味着您不知道您是否在实际运行之前编写正确的语法。我个人对此有一种非常难过的感觉,因为类型安全是我从Typeorm切换到Prisma的主要原因。
  • 您必须将一系列注释分析给属性,还需要在上面的示例中考虑多个属性案例。
  • 存在两种类型的属性,这些属性不适合阅读。
  • 您需要非常谨慎,不要错过使用双重斜杠//评论,这在AST中不存在。

因此,如果不存在完美的解决方案,那么我将构建它。

Zenstack中的属性

ZenStack是一种工具包,可以用强大的访问控制层增强Prisma,并释放其充分的全堆栈开发潜力。它的模式文件ZModel是Prisma架构的超集。

不管Zenstack如何,上面的Prisma文件都应该在下面看起来像:

model User {
    id        String @id
    email     String @email
    password  String @password @omit
    // everyone can signup, and user profile is also publicly readable
    @@allow('create,read', true)
}

实际上,这正是Zmodel所在。您可以在我们的示例代码中找到它:

https://zenstack.dev/docs/get-started/nextjs#3-preparing-the-user-model-for-authentication

上述属性已经在我们的标准库中预定义,如下:

attribute @email()

attribute @password(saltLength: Int?, salt: String?)

attribute @omit()

attribute @@allow(_ operation: String, _ condition: Boolean)

您可以使用相同的方法来定义自己的属性。

Zenstack插件中的访问属性

插件是Zenstack的可扩展性机制。它的用法类似于Prisma中的发电机,但以以下方式有所不同:

  • 它们具有一个清洁界面,没有JSON-RPC的复杂性。
  • 他们使用比发电机更易于编程的AST表示。
  • 他们可以访问Zenstack添加到Prisma的语言功能,例如自定义属性和函数。

插件的实现类似于发电机,但更简单。您只需要创建一个节点模块,该模块以下面的签名导出功能:

import { DMMF } from "@prisma/generator-helper";
import { PluginOptions } from "@zenstackhq/sdk";
import { Model } from "@zenstackhq/sdk/ast";

export default async function run(
  model: Model,
  options: PluginOptions,
  dmmf: DMMF.Document
) {
   ....
}

然后您可以评估以下属性:

const userModel = models.find((m) => m.name === 'User');
const userModelAttributes = userModel?.attributes;

const emailField = userModel?.fields.find((f) => f.name === 'email');
const emailFieldAttributes = emailField?.attributes;

const passwordField = userModel?.fields.find((f) => f.name === 'password');
const passwordFieldAttributes = passwordField?.attributes;

属性具有强大的类型,如下所示:

export interface DataModelFieldAttribute extends AstNode {
    readonly $container: DataModelField;
    readonly $type: 'DataModelFieldAttribute';
    args: Array<AttributeArg>;
    decl: Reference<Attribute>;
}

在内部,使用出色的开源语言工具Langium定义了Zmodel。因此该界面实际上是由langium生成的。

总的来说,您可以使用与Prisma Generator相似的逻辑编写Zenstack插件,该逻辑在您的代码和用户架构文件中使用类型安全。

奖金

您是否注意到在Zenstack的输入功能中,还有一个看起来很熟悉的dmmf参数?是的,这是Prisma的AST模型,与自定义生成器中使用的模型相同。我想这是Zmodel是Prisma模式的超集。但是,如果您在插件中添加代码:

const userModel = dmmf.datamodel.models.find((m) => m.name === 'User');
console.log(`user model attribute: ${userModel?.documentation}`);

const emailField = userModel?.fields.find((f) => f.name === 'email');
console.log(`email field attribute: ${emailField?.documentation}`);

const passwordField = userModel?.fields.find((f) => f.name === 'password');
console.log(`password field attribute: ${passwordField?.documentation}`);

运行后,您将看到与Prisma Generator中完全相同的结果:

user model attribute: @@allow('create,read', true)
password field attribute: @password
@omit

实际上,如果您查看从zmodel生成的schema.prisma文件,您将得到它:

/// @@allow('create,read', true)
model User {
    id String @id() @default(cuid())
    name String?
    /// @email
    email String? @unique()
    /// @password
    /// @omit
    password String?
    zenstack_guard Boolean @default(true)
    zenstack_transaction String?
    @@index([zenstack_transaction])
}

基于的原理,从来没有重新发明轮子 - 我们确实对社区贡献的现有发电机构成了价值,例如Zod Prisma和Prisma-nestjs -graphql。因此,为了使其仍然有效,对于任何非Prisma属性,我们将在schema.prisma文件中作为三重注释生成。

因此,实际的好处是您可以将Zmodel简单地用作纯Prisma +句法自定义属性。,尽管Zenstack拥有的不仅仅是此,但我们仍然很高兴看到任何帮助可以给您! /p>