【问题标题】:How do I apply FlowJS annotations to a Sequelize model?如何将 FlowJS 注释应用于 Sequelize 模型?
【发布时间】:2019-07-21 11:59:05
【问题描述】:

我正在尝试在后端节点项目中将 FlowJS 与 Sequelize 一起使用。但是,没有关于如何注释所涉及的数据类型的具体 FlowJS 文档,我得到的代码可以工作,但也会引发很多类型错误。

我有这个演示项目的完整工作版本on github

我们从一个基本的模型定义开始。我最近一直在玩很多 Stellaris,所以我正在考虑这里的大银河战略。

// @flow

import Sequelize, { type Model } from "sequelize";

export type Empire = {
  name: string,
  species: string,
  homeworld: string
};

const EmpireModel = (sequelize: Sequelize): Class<Model<Empire>> =>
  sequelize.define("empires", {
    name: { type: Sequelize.STRING, allowNull: false },
    species: { type: Sequelize.STRING, allowNull: false },
    homeworld: { type: Sequelize.STRING, allowNull: false }
  });

module.exports = EmpireModel;

就我的想法而言,我想要一个包含所有 Sequelize 函数表操作函数(create、destroy、findAll 等)的数据类型,并且我认为我想要另一种数据类型,它只代表所涉及的实际对象(存储在数据库中的一个特定的帝国)。我可能完全错了,我愿意接受有关如何使上述内容变得更好的建议。

上面的代码类型检查很好,但我不确定Class&lt;Model&lt;Empire&gt;&gt;sequelize.define 返回值的正确注释。

现在我们进入应用程序本身:

// @flow

import Sequelize, { type Model } from "sequelize";
import { type Empire } from "./empire";

const sequelize = new Sequelize(
  "postgres://postgres:sequelize@localhost:5432/sqldemo"
);

const main = async () => {
  const EmpireModel: Class<Model<Empire>> = await require("./empire")(
    sequelize
  );
  const empire: Model<Empire> = await EmpireModel.create({
    name: "Sildaran Republic",
    species: "Sildar",
    homeworld: "Sakatarola"
  });

  const empires = await EmpireModel.findAll();
  console.log(empires);

  empires.map(empire => {
    console.log("[Empire]           ", empire.name);
    console.log("[Founding Species] ", empire.species);
    console.log("[Homeworld]        ", empire.homeworld);
  });
};

这段代码实际上运行正确,但类型检查器在await Empires.findAll() 行标记了一个错误。错误信息是

Cannot call await with `EmpireModel.findAll()` bound to `p` because:
 - Either cannot get `empire.name` because property `name` is missing in `Model` [1].
 - Or cannot call `empires.map` because property `map` is missing in `Promise` [2].

   src/index.js:20:25
     20|   const empires = await EmpireModel.findAll();
                                 ^^^^^^^^^^^^^^^^^^^^^

References:
   flow-typed/npm/sequelize_v4.x.x.js:3264:72
   3264|       options?: FindOptions<TAttributes & TCustomAttributes>): Promise<this[]>,
                                                                                ^^^^ [1]
   flow-typed/npm/sequelize_v4.x.x.js:3264:64
   3264|       options?: FindOptions<TAttributes & TCustomAttributes>): Promise<this[]>,
                                                                        ^^^^^^^^^^^^^^^ [2]

所以,我实际上有几个具体问题:

  1. empires.js 中分配给EmpireModel 的实际正确类型是什么?
  2. await require("./empire")(sequelize); 的返回值的实际类型是什么,以及当我弄错时如何让 Flow 中断(Flow 对我在此处指定的任何类型感到满意,并且仅在以后使用该值时标记错误方式与类型不一致)

我也愿意接受所有其他建议以使此代码更正确。

【问题讨论】:

    标签: javascript node.js sequelize.js flowtype


    【解决方案1】:

    简短的回答:

    对于某些模型x,您必须调用x.get()x.toJSON()1 以获取模型底层的普通对象。如果把查询后的代码改成

      empires.map(empire => empire.get())
             .map(empire => {
               console.log("[Empire]           ", empire.name);
               console.log("[Founding Species] ", empire.species);
               console.log("[Homeworld]        ", empire.homeworld);
             });
    

    然后它会正常工作。

    长答案:

    这个解决方案可能看起来有点武断,所以这是我用来找到它的过程。不幸的是,Flow 的好处是以学习它的局限性和如何调试它的错误消息为代价的,所以希望这能稍微拉开我如何做到这一点的帷幕。

    我总是检查的第一件事是所有类型都正确导入。 flow-typed 目录在错误消息中,Empire 类型也是如此,所以看起来不错。

    接下来,错误消息给了我们两种可能性,我想弄清楚我们正在处理的是哪一种。 empires.map 无效,或者empire.name 无效。所以我注释掉了map中的现有代码,并用标识函数empire =&gt; empire替换了它。当我这样做时,错误消失了,所以很明显问题在于访问empire.name

    为了弄清楚为什么会出现这个问题,我研究了 Sequelize 的 libdef。它位于流式存储库here 中。 Libdefs 看起来很吓人,但它们只是添加了declare 关键字的普通流定义。在普通代码中你可能会说let i : number = 1,而declare你会说

    let i = 1;
    declare i : number
    

    它们之间的区别在于流类型检查普通注释以确保它们有意义,但它将declare 语句视为福音2。考虑到这一点,我们可以阅读 libdef 本身来查看我们正在处理的类型和接口。因为 libdefs 使用declare,所以 Sequelize 包内的实际代码与类型检查无关; Flow 只查看 libdef,而不查看实现。

    这里的 cmets 中有相当详尽的文档,但不幸的是,该文件超过 7k 行,并且没有将最重要的定义放在首位。

    Model 类的声明存在于herefindAll 的定义存在于here。我们可以看到findAll 返回一个this 类型的数组,这意味着一个类型为Model 的数组与我们当前的Model 具有相同的泛型类型参数。回到Model的顶部,我们看到如下参数:

    Model<TAttributes, TInitAttributes = TAttributes, TPlainAttributes = TAttributes>
    

    虽然Model 类型有 3 个参数,但其中两个是可选的,默认设置为第一个参数。

    所以我们的问题是我们得到一个类型为Model&lt;Empire, Empire, Empire&gt; 的对象数组,我们在它上面进行映射,然后我们试图访问在Empire 类型中定义的字段。在无类型 JavaScript 中这很好,因为该库可以动态地将字段添加到模型中,但在 Flow 中,像这样将一种对象类型与另一种对象类型“合并”需要使用交集类型,并且可能会有一些粗糙的边缘情况。幸运的是,查看文件后面的sequelize.define 的定义表明,这里没有做这种花哨的事情。如果我们想要访问包含的Empire,我们需要找到一个返回值为Model的泛型类型参数之一的方法。

    我们可以看到那些here,最后一个定义包含所有可选参数,在简单的答案中为我们提供了解决方案。搜索班级的其他人也会发现toJSON function.


    1 请注意,toJSON 返回一个对象,而不是 JSON 字符串。当一个对象被传递到JSON.stringify 时,stringify 将调用该对象上的toJSON 函数(如果它存在的话),并将改为字符串化toJSON 返回的对象。这就是为什么您可以在 Model 对象上调用 console.log 并查看您在此模型上定义的属性,而不是 Sequelize 附加到模型的所有附加字段。这个名字有误导性,但这是 JavaScript 的错,不是 Sequelize 的错。

    2 从技术上讲,有一些检查。例如,如果你 declare 一个变量同时具有 stringnumber 类型,你会得到一个错误。 declare 注释只检查与其他declare 注释的一致性,然后检查非declare 注释与这些注释的一致性,确保如果有错误,报告为正常注释的问题,而不是@ 987654370@注解。

    【讨论】:

    • 这是一个非常彻底的答案,我很可能会在我将附加到实际项目的文档中链接到它。
    【解决方案2】:

    免责声明:我真的不知道任何 Flow,并且带着 TypeScript 和 Haskell 背景来到这里,不幸的是,TypeScript 并没有对此有任何启示(.define() 的输出只是一个Model&lt;Empire, Empire&gt; 并且它有一个适当的 .findAll() 方法)——但请不要认为我比这更专家。

    如果我查看test suite 的流类型,他们并不总是使用Class&lt;&gt; utility type 来键入s.define() 的输出,而是偶尔使用typeof,这似乎与使用的愿望相对应子类:

    type WarehouseAttributes = {
      id?: number;
      address?: string;
      capacity?: number;
    };
    
    declare class WarehouseInstance extends Model<WarehouseAttributes> {
      id?: number;
      address?: string;
      capacity?: number;
      // ... mixins ...
    };
    
    // ... then this gets used to define ...
    
    let Warehouse: typeof WarehouseInstance = s.define('warehouse', {});
    
    // ... then this gets used to query ...
    
    Warehouse
      .findAll({include: [{association: WarehouseProducts}]})]
      .then((warehouses: Array<WarehouseInstance>) => {})
    

    这种方法是否有可能允许您返回已声明的类,而不是像您现在所做的那样返回 Class&lt;Model&lt;EmpireAttributes&gt;&gt;?也许这对于使Promise&lt;this[]&gt; 具有包含该实例上的Empire 属性的this[] 是必要的?

    【讨论】:

    • 通过调试会话,我们偶然发现Class&lt;Model&lt;EmpireModel&gt;&gt; 似乎对应于整个Empires 表,其中Model&lt;EmpireModel&gt; 似乎对应于一行,包含所有实例方法加上我关心的实际数据字段。我还不知道什么样的光能照亮事物。
    • 实际上,它似乎包含数据字段。我的意思是,确实如此,但 Flow 似乎并不知道这一点。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-10
    • 2011-09-23
    • 1970-01-01
    • 2018-09-16
    • 1970-01-01
    • 2021-05-24
    相关资源
    最近更新 更多