zongo 中文文档教程

发布于 2年前 浏览 14 更新于 2年前

Zongo

去规范化、简化!

利用 zod 创建丰富的、类型齐全的 MongoDB 文档管理 系统。通过文档图的静态分析,反规范化可以成为一种享受而不是一种负担!

目标

  • 在编译时识别和定义文档的约束
  • 利用并扩展 zod 自动创建类型安全
  • 在文档模型上构建静态分析工具
  • 创建嵌入系统,以便轻松进行非规范化建模
  • 自动应用 de - 事务中的规范化更新

入门

您的第一个定义

zongo 中的定义由名称和模式定义。让我们创建一个简单的 具有姓名和年龄的用户定义示例。

// user.ts
import { z } from "zod";
import { zg } from "zongo";

export const userDefinition = zg.createDefinition(
  "User",
  z.strictObject({
    _id: zg.schema.objectId(),
    name: z.string(),
    age: z.number(),
    pets: z.array(z.string()),
  })
);

将其添加到数据库

现在您有了一个简单的定义,您可以通过以下方式将其添加到数据库 将 createDatabase 调用链接到 addDefinition 调用。由此产生的 zdb 变量将包含完全类型化的数据库。

// zdb.ts
import {userDefinition} from "./user";
import {zg} from "zongo";
import {MongoClient} from "mongodb";

const client = new MongoClient(...);
const db = client.db("main");

export const zdb = zg.createDatabase(client, db)
  .addDefinition(userDefinition);

创建文档

创建 zdb 后,它提供了许多用于文档操作的辅助函数。 该库的目标之一是不妨碍 mongodb 本机驱动程序 并且仅在必要时添加类型和帮助程序。创建文档就是其中之一。

// index.ts
import { zdb } from "./zdb";
import { ObjectId } from "mongodb";

async function main() {
  const user = await zdb.create("User", {
    _id: new ObjectId(),
    name: "Daniel",
    age: 22,
    pets: ["George", "Rascal"],
  });
}

void main();

生成的类型 user 将根据 userDefinition 文件中定义的 zod 模式进行完全类型化,并在内部利用 z.output。作为第二个参数传递给 create 的值也在内部使用 z.input 进行严格类型化。

滋润文档

如前所述,zongo 努力尽可能远离 mongodb。鉴于此,zdb 公开了一个 Hydrate 方法,该方法利用原生 mongodb 集合的回调。

import { zdb } from "./zdb";
import { ObjectId } from "mongodb";

async function main() {
  const _id = new ObjectId("...");
  const user = await zdb.hydrate("User", (collection) => {
    return collection.findOne({ _id });
  });
}

void main();

更新文档(通过 zdb)

有两种使用 update 帮助程序更新文档的方法。第一个选项是传递一个对象,其中第一级键将被传递的任何内容覆盖。对于数组操作或多级合并,选项二使用带有可变文档的回调。

import { zdb } from "./zdb";
import { ObjectId } from "mongodb";

async function main() {
  const _id = new ObjectId("...");

  // Option 1
  await zdb.update("User", _id, {
    name: "Danielle",
  });

  // Option 2
  await zdb.update("Update", _id, (doc) => {
    // The doc is accepted to be mutable, no need for immutable updates
    doc.name = "Danielle";
    // Arrays and objects can be directly mutated!
    doc.pets.push("Willow");
    return doc;
  });
}

void main();

创建文档参考

我们当前将宠物存储为字符串数组。这很好,但宠物最好存放在自己的收藏中。让我们为宠物创建一个新集合并将其添加到我们的 zongo 数据库中。

// pet.ts
import { z } from "zod";
import { zg } from "zongo";

export const petDefinition = zg.createDefinition(
  "Pet",
  z.strictObject({
    _id: zg.schema.objectId(),
    name: z.string(),
    type: z.enum(["cat", "horse"]),
    favoriteTreats: z.array(z.string()),
  })
);

// zdb.ts
import {userDefinition} from "./user";
import {petDefinition} from "./pet";
import {zg} from "zongo";
import {MongoClient} from "mongodb";

const client = new MongoClient(...);
const db = client.db("main");

export const zdb = zg.createDatabase(client, db)
  .addDefinition(userDefinition)
  .addDefinition(petDefinition)

用户定义也需要更新。这将利用 zg.schema.document 帮助器。嵌入文档时有几个选项。

  • ref 将仅存储引用文档的 _id
  • partial 将仅存储引用文档中的特定键
  • full 将嵌入整个文档

让我们更新 userDefinition 以存储 petDefinition 的一部分,仅保留 nametype

// user.ts
import { z } from "zod";
import { zg } from "zongo";
import { petDefinition } from "./pet";

export const userDefinition = zg.createDefinition(
  "User",
  z.strictObject({
    _id: zg.schema.objectId(),
    name: z.string(),
    age: z.number(),
    pets: z.array(
      z.schema.document.partial(petDefinition, {
        name: true,
        type: true,
      })
    ),
  })
);

使用引用访问文档

refpartialfull 引用的返回值是 DocumentReference,而不是只是原始值。让我们看看如何利用它。首先让我们重做 main 以创建 3 个文档。

// index.ts
import { zdb } from "./zdb";
import { ObjectId } from "mongodb";

async function main() {
  const george = await zdb.create("Pet", {
    _id: new ObjectId(),
    name: "George",
    type: "cat",
    favoriteTreats: ["cardboard"],
  });
  const rascal = await adb.create("Pet", {
    _id: new ObjectId(),
    name: "Rascal",
    type: "cat",
    favoriteTreats: ["Chipotle cheese"],
  });
  const user = await zdb.create("User", {
    _id: new ObjectId(),
    name: "Daniel",
    age: 22,
    pets: [george, rascal],
  });
}

void main();

接下来,在访问文档时,可以使用DocumentReference类来遍历引用。

// index.ts
import { zdb } from "./zdb";
import { ObjectId } from "mongodb";

async function main() {
  const _id = new ObjectId("...");
  const user = await zdb.hydrate("User", (c) => c.findOne({ _id }));
  for (const pet of user.pets) {
    // Existing data can be accessed synchronously as it is
    // directly embedded with every user
    const { name, type } = pet.getExisting();
    console.log(`${user.name}'s pet ${type} named ${name}`);

    // If more data is needed, `resolve()` will get the
    // full document from the collection
    const { favoriteTreats } = await pet.resolve();
    console.log(`Favorite treats:`);
    for (const treat of favoriteTreats) {
      console.log(`  - ${treat}`);
    }
  }
}

void main();

自动引用更新

当定义中存在引用时,zongo 会处理非规范化写入中最耗时的部分,更新其他文档。例如,更新宠物的名称将需要随后更新用户的 pets 数组中包含该宠物的名称。这可能会变得乏味并且容易出错。

使用 zongo,您的架构将被静态分析,并且可以自动高效地执行后续更新。文档在架构中的嵌套方式没有任何限制。嵌套数组和对象都将正确更新。

例如,使用 zdb.update 更新宠物将自动更新对其的所有引用。

import { zdb } from "./zdb";
import { ObjectId } from "mongodb";

async function main() {
  const _userId = new ObjectId("...");
  const _petId = new ObjectId("...");

  await zdb.update("Pet", _petId, {
    name: "Georgie",
  });

  const user = await zdb.hydrate("Pet", (c) => c.findOne({ _id: _petId }));
  const { name } = user.pet.find((p) => p.id === _petId)!.getExisting();
  asset(name === "Georgie");
}

void main();

请注意上面的代码中,两个文档在 zdb.update() 步骤期间如何更新。不仅“乔治”宠物文档更新了,“丹尼尔”用户也收到了更新。 name 字段位于用户定义的 pet 部分中,因此需要更新,否则猫将是用户文档中的 George 以及 Georgie > 在宠物文件上。通过使用 zongo 更新方法可以完全避免这种不一致。 ????

结构

该库中的所有内容都将基于 zg 导出。可以通过两种方式导入

  1. import zg from "zongo";
  2. import {zg} from "zongo";

这两种方法都受支持且有效,但智能感知效果更好具有严格定义的名称,因此第二个选项用于自动完成。选项 1 不需要命名为 zg,尽管我强烈推荐它???

基本导入

基本 zg 导入包含所有“创建”函数。这些将用于实例化新的东西,例如 数据库、集合定义或部分定义。

import {zg} from "zongo";

zg.createDefinition(...);
zg.createPartial(...);
zg.createDatabase(...);

zg.schema 模块

zg.schema 模块中,将在 zod 上下文中使用的所有函数 可以找到架构。

进一步深入,有两个子模块 schematypes

API(WIP)

zg

zg.createDatabase

zg.createDefinition

zg.createPartial

zg.schema

zg.schema.document

zg.schema.document.full
zg.schema.document.partial
zg.schema.document .ref

zg.schema.objectId

只是 z.instanceOf(mongo.ObjectId) 的别名。它在文档创建中被大量使用,在这里它只是作为别名助手。此函数没有任何内部意义,如果您希望使用 z.instanceOf(mongo.ObjectId) 来代替,请继续。

import { zg } from "zongo";
import { z } from "zod";

const schema = z.strictObject({
  _id: zg.schema.objectId(),
});

zg.schema.partial

zg.schema.partial<PD extends zg.types.PartialDefinition<any>>(partial: PD);

将部分实例化为 zod 模式。

import { zg } from "zongo";
import { z } from "zod";

const AuditEntry = z.createPartial(
  "AuditEntry",
  z.strictObject({
    action: z.string(),
    timestamp: z.date(),
  })
);

const schema = z.strictObject({
  auditLog: z.array(zg.partial(AuditEntry)),
});

Zongo

De-normalization, simplified!

Utilizing zod to create a rich, fully-typed MongoDB document management system. With static analysis of the document graph, de-normalization can become a treat rather than a burden!

Goals

  • Identify and define constraints of the documents at compile time
  • Utilize and expand on zod to automatically create type safety
  • Build static analysis tools on the document models
  • Create embedding system that allows for easy de-normalized modelling
  • Automatically apply de-normalized updates in a transaction

Getting Started

Your first definition

Definitions in zongo are defined by a name and a schema. Let's create a simple example for a user definition with a name and age.

// user.ts
import { z } from "zod";
import { zg } from "zongo";

export const userDefinition = zg.createDefinition(
  "User",
  z.strictObject({
    _id: zg.schema.objectId(),
    name: z.string(),
    age: z.number(),
    pets: z.array(z.string()),
  })
);

Adding it to a database

Now that you have a simple definition you can add it to the database by chaining a createDatabase call to a addDefinition call. The resultant zdb variable will contain the fully typed database.

// zdb.ts
import {userDefinition} from "./user";
import {zg} from "zongo";
import {MongoClient} from "mongodb";

const client = new MongoClient(...);
const db = client.db("main");

export const zdb = zg.createDatabase(client, db)
  .addDefinition(userDefinition);

Creating a document

After creating your zdb it provides many helper functions for document manipulation. One of the goals of this library is to stay "out of the way" of the mongodb native driver and only add typings and helpers where necessary. Creating a document is one of them.

// index.ts
import { zdb } from "./zdb";
import { ObjectId } from "mongodb";

async function main() {
  const user = await zdb.create("User", {
    _id: new ObjectId(),
    name: "Daniel",
    age: 22,
    pets: ["George", "Rascal"],
  });
}

void main();

The resultant type user will be fully typed based on the zod schema defined in the userDefinition file, utilizing z.output<typeof schema> internally. The value passed as the second argument to create is also strictly typed with z.input<typeof schema> internally.

Hydrating a document

As mentioned previously, zongo strives to stay out of the way from mongodb as much as possible. Given that, zdb exposes a hydrate method that utilizes a callback with a native mongodb collection.

import { zdb } from "./zdb";
import { ObjectId } from "mongodb";

async function main() {
  const _id = new ObjectId("...");
  const user = await zdb.hydrate("User", (collection) => {
    return collection.findOne({ _id });
  });
}

void main();

Updating a document (via zdb)

There are two ways to update a document using the update helper. The first option is to pass an object, where the first-level keys will be overridden by whatever is passed. For array manipulation or multi-level merging, option two utilizes a callback with a mutable document.

import { zdb } from "./zdb";
import { ObjectId } from "mongodb";

async function main() {
  const _id = new ObjectId("...");

  // Option 1
  await zdb.update("User", _id, {
    name: "Danielle",
  });

  // Option 2
  await zdb.update("Update", _id, (doc) => {
    // The doc is accepted to be mutable, no need for immutable updates
    doc.name = "Danielle";
    // Arrays and objects can be directly mutated!
    doc.pets.push("Willow");
    return doc;
  });
}

void main();

Creating a document reference

We are currently storing pets as an array of strings. This is great, but pets might be better stored in their own collection. Let's create a new collection for the pets and added to our zongo database.

// pet.ts
import { z } from "zod";
import { zg } from "zongo";

export const petDefinition = zg.createDefinition(
  "Pet",
  z.strictObject({
    _id: zg.schema.objectId(),
    name: z.string(),
    type: z.enum(["cat", "horse"]),
    favoriteTreats: z.array(z.string()),
  })
);

// zdb.ts
import {userDefinition} from "./user";
import {petDefinition} from "./pet";
import {zg} from "zongo";
import {MongoClient} from "mongodb";

const client = new MongoClient(...);
const db = client.db("main");

export const zdb = zg.createDatabase(client, db)
  .addDefinition(userDefinition)
  .addDefinition(petDefinition)

The user definition needs to be updated as well. This will utilize the zg.schema.document helpers. There are a few options when embedding a document.

  • ref will only store the _id of the referenced document
  • partial will only store specific keys from the referenced document
  • full will embed the entire document

Let's update the userDefinition to store a partial of the petDefinition, only keeping the name and type.

// user.ts
import { z } from "zod";
import { zg } from "zongo";
import { petDefinition } from "./pet";

export const userDefinition = zg.createDefinition(
  "User",
  z.strictObject({
    _id: zg.schema.objectId(),
    name: z.string(),
    age: z.number(),
    pets: z.array(
      z.schema.document.partial(petDefinition, {
        name: true,
        type: true,
      })
    ),
  })
);

Accessing a document with references

The return value of a ref, partial, and full reference is a DocumentReference, not just the raw values. Let's see how that is utilized. First let's redo our main to create 3 documents.

// index.ts
import { zdb } from "./zdb";
import { ObjectId } from "mongodb";

async function main() {
  const george = await zdb.create("Pet", {
    _id: new ObjectId(),
    name: "George",
    type: "cat",
    favoriteTreats: ["cardboard"],
  });
  const rascal = await adb.create("Pet", {
    _id: new ObjectId(),
    name: "Rascal",
    type: "cat",
    favoriteTreats: ["Chipotle cheese"],
  });
  const user = await zdb.create("User", {
    _id: new ObjectId(),
    name: "Daniel",
    age: 22,
    pets: [george, rascal],
  });
}

void main();

Next, when accessing the document, the references can be traversed with the DocumentReference class.

// index.ts
import { zdb } from "./zdb";
import { ObjectId } from "mongodb";

async function main() {
  const _id = new ObjectId("...");
  const user = await zdb.hydrate("User", (c) => c.findOne({ _id }));
  for (const pet of user.pets) {
    // Existing data can be accessed synchronously as it is
    // directly embedded with every user
    const { name, type } = pet.getExisting();
    console.log(`${user.name}'s pet ${type} named ${name}`);

    // If more data is needed, `resolve()` will get the
    // full document from the collection
    const { favoriteTreats } = await pet.resolve();
    console.log(`Favorite treats:`);
    for (const treat of favoriteTreats) {
      console.log(`  - ${treat}`);
    }
  }
}

void main();

Automatic reference updates

When references exist within the definitions, zongo handles the most time consuming part about de-normalized writes, updating other documents. For example, an update to a pet's name would require a subsequent update to the user's with that pet in their pets array. This can become tedious and is prone to errors.

With zongo, your schema is statically analyzed and can perform the subsequent updates automatically and efficiently. There are no restrictions on the way a document is nested within your schema. Nested arrays and objects will both update properly.

For example, updating a pet using zdb.update will automatically update all references to it.

import { zdb } from "./zdb";
import { ObjectId } from "mongodb";

async function main() {
  const _userId = new ObjectId("...");
  const _petId = new ObjectId("...");

  await zdb.update("Pet", _petId, {
    name: "Georgie",
  });

  const user = await zdb.hydrate("Pet", (c) => c.findOne({ _id: _petId }));
  const { name } = user.pet.find((p) => p.id === _petId)!.getExisting();
  asset(name === "Georgie");
}

void main();

Note how in the code above, two documents were updated during the zdb.update() step. Not only was the "George" pet document updated, but the "Daniel" user received an update too. The name field was within the pet partial on the user definition, so it required an update or the cat would be George on the user document and Georgie on the pet document. This inconsistency is completely avoided by utilizing the zongo update method. ????

Structure

Everything in this library will be based off the zg export. This can be imported in two ways

  1. import zg from "zongo";
  2. import {zg} from "zongo";

Both of these methods are supported and valid, but intellisense works better with strictly defined names so the second option is there for autocomplete. Option 1 has no requirement to be named zg, though I highly recommend it ????

Base Import

The base zg import contains all of the "create" functions. These will be used to instantiate something new, like a database, collection definition, or partial definition.

import {zg} from "zongo";

zg.createDefinition(...);
zg.createPartial(...);
zg.createDatabase(...);

zg.schema module

In the zg.schema module, all functions that will be utilized within the context of a zod schema can be found.

Drilling down further, there are two submodules schema and types.

API (WIP)

zg

zg.createDatabase

zg.createDefinition

zg.createPartial

zg.schema

zg.schema.document

zg.schema.document.full
zg.schema.document.partial
zg.schema.document.ref

zg.schema.objectId

Simply an alias to z.instanceOf(mongo.ObjectId). It is used quite a lot in document creation, to it is here simply as an alias helper. There is no internal significance of this function and if you wish to use z.instanceOf(mongo.ObjectId) instead, go ahead.

import { zg } from "zongo";
import { z } from "zod";

const schema = z.strictObject({
  _id: zg.schema.objectId(),
});

zg.schema.partial

zg.schema.partial<PD extends zg.types.PartialDefinition<any>>(partial: PD);

Instantiates a partial into a zod schema.

import { zg } from "zongo";
import { z } from "zod";

const AuditEntry = z.createPartial(
  "AuditEntry",
  z.strictObject({
    action: z.string(),
    timestamp: z.date(),
  })
);

const schema = z.strictObject({
  auditLog: z.array(zg.partial(AuditEntry)),
});
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文