发布于 2026-01-06 9 阅读
0

【更新】DynamoDB-Toolbox v1 beta 版上线啦🙌 你需要知道的一切!

【更新】DynamoDB-Toolbox v1 beta 版上线啦🙌 你需要知道的一切!

☝️注意:本文是关于版本beta.1发布的。

👉如果您需要有关该beta.0版本的文档,您可能需要查找本文的先前版本

👉如果您想从 迁移beta.0beta.1,有一篇专门的文章总结了这些变化。

Theodo ,我们都是 Jeremy Daly 的DynamoDB Toolbox的忠实粉丝。我们早在 2019 年就开始使用它,并且越来越喜欢它……但我们也很清楚它的不足之处😅

其中一个原因是它最初是用 JavaScript 编写的。虽然 Jeremy 在 2020 年用 TypeScript 重写了源代码,但它没有处理类型推断,而我最终在v0.4版本中自己实现了这个功能。

然而,我们仍然觉得缺少一些功能:从声明enums原始类型,到支持递归模式和类型(列表和映射子属性)以及多态性

我也曾对面向对象的方法有所顾虑:我并不反对类,但它们无法进行摇树优化(tree-shaking)。这意味着在无服务器环境中,它们应该保持相对轻量级。AWS 在其 SDK v3中就采用了这种方法,而且理由充分:保持包的精简!

但 DynamoDB-Toolbox 的情况并非如此:我记得我曾经编写过一个.update超过 1000 行的方法……但为什么要把它打包在一起,而你根本不需要它呢?

因此,去年我决定全身心投入到代码的彻底改造中,主要目标有三个:

今天,我很高兴地宣布dynamodb-toolbox v1 beta 版发布🙌 它包含了重新设计的TableEntity,以及对 ` get_require`、 ` get_require``get_require` 命令(包括条件和投影)的完整支持,`get_require`PutItem` get_require` 命令也将很快推出。GetItemUpdateItemDeleteItemQueryScan

本文详细介绍了新 API 的工作原理以及与先前版本相比的主要变更——顺便一提,这些变更仅涉及 API:无需数据迁移🥳

让我们开始吧!

目录

安装

### npm
npm i dynamodb-toolbox@1.0.0-beta.1

## yarn
yarn add dynamodb-toolbox@1.0.0-beta.1

## ...and so on
Enter fullscreen mode Exit fullscreen mode

v1工具基于v3AWS SDK 构建。它依赖于 AWS SDK@aws-sdk/client-dynamodb@aws-sdk/lib-dynamodbAWS 开发工具包,因此您也需要安装它们:

## npm
npm i @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

## yarn
yarn add @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

## ...and so on
Enter fullscreen mode Exit fullscreen mode

表格

表格的定义方式与之前的版本基本相同,但key现在属性type旁边多了一个冒号name

import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
// Will be renamed Table in the official release 😉
import { TableV2 } from 'dynamodb-toolbox';

const dynamoDBClient = new DynamoDBClient({});
const documentClient = DynamoDBDocumentClient.from(dynamoDBClient);

const myTable = new TableV2({
  name: 'MySuperTable',
  partitionKey: {
    name: 'PK',
    type: 'string', // 'string' | 'number' | 'binary'
  },
  sortKey: {
    name: 'SK',
    type: 'string',
  },
  documentClient,
});
Enter fullscreen mode Exit fullscreen mode

☝️ v1 版本尚不支持索引,因为查询功能尚未推出。

可以通过 getter 方法提供表名,这在某些情况下非常有用,例如在不实际运行任何命令的情况下使用该类(例如测试或部署):

const myTable = new TableV2({
  ...
  // 👇 Only executed at command execution
  name: () => process.env.TABLE_NAME,
});
Enter fullscreen mode Exit fullscreen mode

与之前的版本一样,这些类会通过一个内部字符串属性(默认保存为 ` <object_id> v1`)为数据添加实体标识符。可以通过参数在类级别重命名该属性entity"_et"TableentityAttributeSavedAs

const myTable = new TableV2({
  ...
  // 👇 defaults to "_et"
  entityAttributeSavedAs: '__entity__',
});
Enter fullscreen mode Exit fullscreen mode

实体

对于实体而言,主要变化在于attributes参数变为schema

// Will be renamed Entity in the official release 😉
import { EntityV2, schema } from 'dynamodb-toolbox';

const myEntity = new EntityV2({
  name: 'MyEntity',
  table: myTable,
  // Attributes definition
  schema: schema({ ... }),
});
Enter fullscreen mode Exit fullscreen mode

时间戳

内部时间戳属性也存在,其行为与之前的版本类似。您可以设置timestamps`to`false来禁用它们(默认值为 `false` true),或者微调 ` createdand`modified属性名称:

const myEntity = new EntityV2({
  ...
  // 👇 de-activate timestamps altogether
  timestamps: false,
});

const myEntity = new EntityV2({
  ...
  timestamps: {
    // 👇 de-activate only `created` attribute
    created: false,
    modified: true,
  },
});

const myEntity = new EntityV2({
  ...
  timestamps: {
    created: {
      // 👇 defaults to "created"
      name: 'creationDate',
      // 👇 defaults to "_ct"
      savedAs: '__createdAt__',
    },
    modified: {
      // 👇 defaults to "modified"
      name: 'lastModificationDate',
      // 👇 defaults to "_md"
      savedAs: '__lastMod__',
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

匹配表架构

与之前的版本相比,一个重要的变化是,EntityV2关键属性会根据TableV2模式进行验证,验证方式包括类型验证和运行时验证。有两种方法可以匹配表模式:

  • 最简单的方法是使用一个与表模式相匹配的实体模式(参见“设计实体模式”)。这样,该实体就被认为是有效的,不需要其他参数:
import { string } from 'dynamodb-toolbox';

const pokemonEntity = new EntityV2({
  name: 'Pokemon',
  table: myTable, // <= { PK: string, SK: string } primary key
  schema: schema({
    // Provide a schema that matches the primary key
    PK: string().key(),
    // 🙌 using "savedAs" will also work
    pokemonId: string().key().savedAs('SK'),
    ...
  }),
});
Enter fullscreen mode Exit fullscreen mode
  • 如果实体键属性与表架构不匹配,则该类Entity会要求您添加一个computeKey属性,该属性必须从实体键属性派生出主键:
const pokemonEntity = new EntityV2({
  ...
  table: myTable, // <= { PK: string, SK: string } primary key
  schema: schema({
    pokemonClass: string().key(),
    pokemonId: string().key(),
    ...
  }),
  // 🙌 `computeKey` is correctly typed
  computeKey: ({ pokemonClass, pokemonId }) => ({
    PK: pokemonClass,
    SK: pokemonId,
  }),
});
Enter fullscreen mode Exit fullscreen mode

SavedItem 和 FormattedItem

如果您感到迷茫,您可以随时使用 ` SavedItemand` 和 ` FormattedItemutility` 类型来推断实体项的类型:

import type { FormattedItem, SavedItem } from 'dynamodb-toolbox';

const pokemonEntity = new EntityV2({
  name: 'Pokemon',
  timestamps: true,
  table: myTable,
  schema: schema({
    pokemonClass: string().key().savedAs('PK'),
    pokemonId: string().key().savedAs('SK'),
    level: number().default(1),
    customName: string().optional(),
    internalField: string().hidden(),
  }),
});

// What Pokemons will look like in DynamoDB
type SavedPokemon = SavedItem<typeof pokemonEntity>;
// 🙌 Equivalent to:
// {
//   _et: "Pokemon",
//   _ct: string,
//   _md: string,
//   PK: string,
//   SK: string,
//   level: number,
//   customName?: string | undefined,
//   internalField: string | undefined,
// }

// What fetched Pokemons will look like in your code
type FormattedPokemon = FormattedItem<typeof pokemonEntity>;
// 🙌 Equivalent to:
// {
//   created: string,
//   modified: string,
//   pokemonClass: string,
//   pokemonId: string,
//   level: number,
//   customName?: string | undefined,
// }
Enter fullscreen mode Exit fullscreen mode

设计实体模式

现在让我们深入了解一下改动最大的部分:模式定义

模式定义

与zodyup类似,属性现在通过函数构建器定义。对于 TypeScript 用户来说,这消除了as const之前用于类型推断的语句(所以迁移时别忘了删除它🙈)。

您可以通过专用的导入语句导入属性构建器,也可以使用attribute简写attr形式。例如,以下两种声明将输出相同的属性模式:

import { string, attribute, attr } from 'dynamodb-toolbox';

// 👇 More tree-shakable
const pokemonName = string();
// 👇 Not tree-shakable, but single import
const pokemonName = attribute.string();
const pokemonName = attr.string();
Enter fullscreen mode Exit fullscreen mode

在被声明包裹之前schema,属性被称为“温属性”:它们未经(运行时)验证,可用于构建其他模式。通过检查它们的类型,你会发现它们带有前缀$。一旦被冻结,就会应用验证并移除构建方法。

温控模式与冷冻模式

主要结论是,热模式可以组合,冻结模式则不能

import { schema } from 'dynamodb-toolbox';

const pokemonName = string();

const pokemonSchema = schema({
  // 👍 No problem
  pokemonName,
  ...
});

const pokedexSchema = schema({
  // ❌ Not possible
  pokemon: pokemonSchema,
  ...
});
Enter fullscreen mode Exit fullscreen mode

您可以使用专用方法或提供选项对​​象来创建/更新预配置属性。前者提供了一个简洁的 devX 界面,支持自动完成和简写,而后者理论上需要的计算时间和内存使用量更少,尽管实际影响应该非常小(验证仅在冻结时应用):

// Using methods
const pokemonName = string().required('always');
// Using options
const pokemonName = string({ required: 'always' });
Enter fullscreen mode Exit fullscreen mode

所有属性都具有以下选项:

  • required (字符串?="atLeastOnce")根据需要标记根属性或映射子属性。可能的值包括:
    • "atLeastOnce"PutItem命令中必需
    • "never":在所有命令中均为可选
    • "always":所有命令都必须包含
// Equivalent
const pokemonName = string().required();
const pokemonName = string({ required: 'atLeastOnce' });

// `.optional()` is a shorthand for `.required(”never”)`
const pokemonName = string().optional();
const pokemonName = string({ required: 'never' });
Enter fullscreen mode Exit fullscreen mode

与之前的版本相比,一个非常重要的重大变更在于,现在默认情况下必须包含根属性和 Map 子属性。这样做是为了更好地协同工作,实现组合和验证功能

💡除了根属性和 Map 子属性(例如字符串列表)之外,子模式不应该是可选的。所以,我应该强制用户list(string().required())每次都写子模式,还是应该让字符串验证和类型推断能够感知上下文(在列表中忽略子模式,但在 Map 中保留)?我觉得默认强制要求子模式并阻止类似这样的模式required会更优雅string()list(string().optional())

  • hidden (boolean?=true)格式化命令返回项时跳过该属性:
const pokemonName = string().hidden();
const pokemonName = string({ hidden: true });
Enter fullscreen mode Exit fullscreen mode
  • key (布尔值?=true)根据需要添加用于计算主键的标签属性:
// Note: The method will also modify the `required` property to "always"
// (it is often the case in practice, you can still use `.optional()` if needed)
const pokemonName = string().key();
const pokemonName = string({ key: true, required: 'always' });
Enter fullscreen mode Exit fullscreen mode
  • savedAs (字符串)以前称为map。在发送命令之前重命名根属性或 Map 子属性:
const pokemonName = string().savedAs('_n');
const pokemonName = string({ savedAs: '_n' });
Enter fullscreen mode Exit fullscreen mode

属性类型

以下是所有可用属性类型的完整列表:

任何

定义一个任意值的属性。运行时不会进行任何验证,其类型将解析为unknown

import { any } from 'dynamodb-toolbox';

const pokemonSchema = schema({
  ...
  metadata: any(),
});

type FormattedPokemon = FormattedItem<typeof pokemonEntity>;
// => {
//   ...
//   metadata: unknown
// }
Enter fullscreen mode Exit fullscreen mode

您可以通过defaults选项或keyDefault方法putDefault提供默认值updateDefault。此外,还提供了一个更简单的default方法。它的作用类似于putDefault,但如果属性已被标记为key属性,则它将作为keyDefault

const metadata = any().default({ any: 'value' });
// 👇 Similar to
const metadata = any().putDefault({ any: 'value' });
// 👇 ...or
const metadata = any({
  defaults: {
    key: undefined,
    put: { any: 'value' },
    update: undefined,
  },
});

const keyPart = any().key().default('my-awesome-partition-key');
// 👇 Similar to
const metadata = any().key().keyDefault('my-awesome-partition-key');
// 👇 ...or
const metadata = any({
  key: true,
  defaults: {
    key: 'my-awesome-partition-key',
    // put & update defaults are not useful in `key` attributes
    put: undefined,
    update: undefined,
  },
});

const metadata = any().default(() => 'Getters also work!');
Enter fullscreen mode Exit fullscreen mode

基本元素

定义一个stringnumber属性booleanbinary

import { string, number, boolean, binary } from 'dynamodb-toolbox';

const pokemonSchema = schema({
  ...
  pokemonType: string(),
  level: number(),
  isLegendary: boolean(),
  binEncoded: binary(),
});

type FormattedPokemon = FormattedItem<typeof pokemonEntity>;
// => {
//   ...
//   pokemonType: string
//   level: number
//   isLegendary: boolean
//   binEncoded: Buffer
// }
Enter fullscreen mode Exit fullscreen mode

与属性类似any,您可以通过defaults选项或default方法提供默认值:

// 🙌 Correctly typed!
const creationDate = string().default(() => new Date().toISOString());
// 👇 Similar to
const creationDate = string().putDefault(() => new Date().toISOString());
// 👇 ...or
const creationDate = string({
  defaults: {
    key: undefined,
    put: () => new Date().toISOString(),
    update: undefined,
  },
});

// 👇 Additionally fill 'creationDate' on updates if needed
import { $get } from 'dynamodb-toolbox';

const creationDate = string()
  .putDefault(() => new Date().toISOString())
  // (See UpdateItemCommand section for $get description)
  .updateDefault(() => $get('creationDate', new Date().toISOString()));
// 👇 Similar to
const creationDate = string({
  defaults: {
    key: undefined,
    put: () => new Date().toISOString(),
    update: () => $get('creationDate', new Date().toISOString()),
  },
});

const id = number().key().default(1);
// 👇 Similar to
const id = number().key().keyDefault(1);
// 👇 ...or
const id = number({
  defaults: {
    key: 1,
    // put & update defaults are not useful in `key` attributes
    put: undefined,
    update: undefined,
  },
});
Enter fullscreen mode Exit fullscreen mode

原始类型还有一个额外的enum选项。例如,您可以提供一个有限的宝可梦类型列表:

const pokemonTypeAttribute = string().enum('fire', 'grass', 'water');

// Shorthand for `.enum("POKEMON").default("POKEMON")`
const pokemonPartitionKey = string().const('POKEMON');
Enter fullscreen mode Exit fullscreen mode

💡出于类型推断的考虑,该enum选项仅作为方法可用,不作为对象选项可用。

定义一组字符串、数字或二进制数据。与之前的版本不同,集合现在以Set类的形式存在。如果您更倾向于使用数组(或者希望两者都能使用),请告诉我:

import { set } from 'dynamodb-toolbox';

const pokemonSchema = schema({
  ...
  skills: set(string()),
});

type FormattedPokemon = FormattedItem<typeof pokemonEntity>;
// => {
//   ...
//   skills: Set<string>
// }
Enter fullscreen mode Exit fullscreen mode

选项可以作为第二个参数提供:

const setAttr = set(string()).hidden();
const setAttr = set(string(), { hidden: true });
Enter fullscreen mode Exit fullscreen mode

列表

定义任意类型的子模式列表:

import { list } from 'dynamodb-toolbox';

const pokemonSchema = schema({
  ...
  skills: list(string()),
});

type FormattedPokemon = FormattedItem<typeof pokemonEntity>;
// => {
//   ...
//   skills: string[]
// }
Enter fullscreen mode Exit fullscreen mode

与集合一样,选项可以作为第二个参数提供。

地图

定义一个有限的键值对列表。键必须遵循字符串模式,而值可以是任何类型的子模式:

import { map } from 'dynamodb-toolbox';

const pokemonSchema = schema({
  ...
  nestedMagic: map({
    will: map({
      work: string().const('!'),
    }),
  }),
});

type FormattedPokemon = FormattedItem<typeof pokemonEntity>;
// => {
//   ...
//   nestedMagic: {
//     will: {
//       work: "!"
//     }
//   }
// }
Enter fullscreen mode Exit fullscreen mode

与集合和列表一样,选项可以作为第二个参数提供。

记录

记录( Record)是Partial<Record<KeyType, ValueType>>TypeScript 中一种新的属性类型。记录与映射(Map)不同,它们可以接受无限范围的键,并且始终是部分值:

import { record } from 'dynamodb-toolbox';

const pokemonType = string().enum(...);

const pokemonSchema = schema({
  ...
  weaknessesByPokemonType: record(pokemonType, number()),
});

type FormattedPokemon = FormattedItem<typeof pokemonEntity>;
// => {
//   ...
//   weaknessesByPokemonType: {
//     [key in PokemonType]?: number
//   }
// }
Enter fullscreen mode Exit fullscreen mode

选项可以作为第三个参数提供:

const recordAttr = record(string(), number()).hidden();
const recordAttr = record(string(), number(), { hidden: true });
Enter fullscreen mode Exit fullscreen mode

任何

一种新的属性类型,表示类型的并集,即一系列可能的类型:

import { anyOf } from 'dynamodb-toolbox';

const pokemonSchema = schema({
  ...
  pokemonType: anyOf([
    string().const('fire'),
    string().const('grass'),
    string().const('water'),
  ]),
});
Enter fullscreen mode Exit fullscreen mode

在这个特定情况下,`a`enum就能解决问题。然而,当与 `a`原始属性的`or`指令anyOf结合使用时,`a` 会变得特别强大,从而实现多态性mapenumconst

const pokemonSchema = schema({
  ...
  captureState: anyOf([
    map({
      status: string().const('caught'),
      // 👇 captureState.trainerId exists if status is "caught"...
      trainerId: string(),
    }),
    // ...but not otherwise! 🙌
    map({ status: string().const('wild') }),
  ]),
});

type CaptureState = FormattedItem<typeof pokemonEntity>['captureState'];
// 🙌 Equivalent to:
// | { status: "wild" }
// | { status: "caught", trainerId: string }
Enter fullscreen mode Exit fullscreen mode

与集合、列表和映射一样,选项可以作为第二个参数提供。

期待

暂时就这些!我计划在某个时候添加新nulltuple属性。allOf

如果您希望看到其他类型,欢迎在本文章下方留言,或在官方代码仓库中发起讨论并添加v1👍 标签。

计算出的默认值

在之前的版本中,default该功能用于根据其他属性值计算属性值。对于复合索引等“技术性”属性,此功能非常实用。

然而,在 TypeScript 中正确输入代码是不可能的:

const pokemonSchema = schema({
  ...
  level: number(),
  levelPlusOne: number().default(
    // ❌ No way to retrieve the caller context
    input => input.level + 1,
  ),
});
Enter fullscreen mode Exit fullscreen mode

这意味着input输入的内容被随意输入,而正确输入则成了开发人员的责任,这对我来说是无法接受的。

我最终采用的解决方案是将计算默认值的声明拆分为两个步骤:

  • 首先,声明属性默认值应派生自其他属性
import { ComputedDefault } from 'dynamodb-toolbox';

const pokemonSchema = schema({
  ...
  level: number(),
  levelPlusOne: number().default(ComputedDefault),
});
Enter fullscreen mode Exit fullscreen mode

💡ComputedDefault是一个 JavaScript符号(TLDR:一种独特且自定义的符号null),因此它不可能与实际所需的默认值冲突。

  • 然后,通过属性声明一种在实体级别计算此属性的方法putDefaults
const pokemonEntity = new EntityV2({
  ...
  schema: pokemonSchema,
  putDefaults: {
    // 🙌 Correctly typed!
    levelPlusOne: ({ level }) => level + 1,
  },
});
Enter fullscreen mode Exit fullscreen mode
  • 同样的情况也适用于updateDefaults(或两者):
import { $get } from 'dynamodb-toolbox';

const pokemonSchema = schema({
  ...
  level: number(),
  previousLevel: number().updateDefault(ComputedDefault),
});

const pokemonEntity = new EntityV2({
  ...
  schema: pokemonSchema,
  updateDefaults: {
    // 🙌 Correctly typed!
    previousLevel: ({ level }) =>
      // Update 'previousLevel' only if 'level' is updated
      level !== undefined ? $get('level') : undefined,
  },
});
Enter fullscreen mode Exit fullscreen mode

在嵌套属性这种棘手的情况下,putDefaults它们会变成带有`or`属性的updateDefaults对象,以强调计算是局部的_attributes_elements

const pokemonSchema = schema({
  ...
  defaultLevel: number(),
  // 👇 Defaulted Map attribute
  levelHistory: map({
    currentLevel: number(),
    // 👇 Defaulted sub-attribute
    nextLevel: number().default(ComputedDefault),
  }).default(ComputedDefault),
});

const pokemonEntity = new EntityV2({
  ...
  schema: pokemonSchema,
  putDefaults: {
    levelHistory: {
      // Defaulted value of Map attribute
      _map: item => ({
        currentLevel: item.defaultLevel,
        nextLevel: item.defaultLevel,
      }),
      _attributes: {
        // Defaulted value of sub-attribute
        nextLevel: (levelHistory, item) => levelHistory.currentLevel + 1,
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

命令

既然我们已经了解了如何设计实体,接下来让我们看看如何利用它们来编写命令👍

💡测试版仅支持 ` PutItem、` GetItem、`UpdateItemDeleteItem` 命令。如果您需要运行`QueryScan` 命令,我的建议是运行原生 SDK 命令并使用 ` formatSavedItemutil`格式化其输出。

正如引言中所述,我一直在寻找一种有利于摇树优化的语法。以下是一个示例,命令如下PutItem

// v0.x Not tree-shakable
const response = await pokemonEntity.put(pokemonItem, options);

// v1 Tree-shakable 🙌
import { PutItemCommand } from 'dynamodb-toolbox';

const command = new PutItemCommand(
  pokemonEntity,
  // 🙌 Correctly typed!
  pokemonItem,
  // 👇 Optional
  putItemOptions,
);

// Get command params
const params = command.params();
// Send command
const response = await command.send();
Enter fullscreen mode Exit fullscreen mode

pokemonItem可以稍后提供或编辑:

import { PutItemCommand } from 'dynamodb-toolbox';

const incompleteCommand = new PutItemCommand(pokemonEntity);

// (will return a new command and not mutate the original one)
const completeCommand = incompleteCommand.item(pokemonItem);

// (can be chained by design)
const response = await incompleteCommand
  .item(pokemonItem)
  .options(options)
  .send();
Enter fullscreen mode Exit fullscreen mode

您还可以使用.build实体的方法来直接构造一个与您的实体关联的命令:

// 🙌 We get a syntax closer to v0.x... but tree-shakable!
const response = await pokemonEntity
  .build(PutItemCommand)
  .item(pokemonItem)
  .options(options)
  .send();
Enter fullscreen mode Exit fullscreen mode

PutItemCommand

选项的行为与之前的版本完全相同。该选项受益于更简洁capacity输入方式和更清晰的逻辑组合:metricsreturnValuescondition

import { PutItemCommand } from 'dynamodb-toolbox';

const { Attributes } = await pokemonEntity
  .build(PutItemCommand)
  .item(pokemonItem)
  .options({
    capacity: 'TOTAL',
    metrics: 'SIZE',
    // 👇 Will type the response `Attributes`
    returnValues: 'ALL_OLD',
    condition: {
      or: [
        { attr: 'pokemonId', exists: false },
        // 🙌 "lte" is correcly typed
        { attr: 'level', lte: 99 },
        // 🙌 You can nest logical combinations
        { and: [{ not: { ... } }, ...] },
      ],
    },
  })
  .send();
Enter fullscreen mode Exit fullscreen mode

获取物品命令

attributes选项的功能与之前的版本相同,但同时也受益于改进的输入方式:

import { GetItemCommand } from 'dynamodb-toolbox';

const { Item } = await pokemonEntity
  .build(GetItemCommand)
  .key(pokemonKey)
  .options({
    capacity: 'TOTAL',
    consistent: true,
    // 👇 Will type the response `Item`
    attributes: ['pokemonId', 'pokemonType', 'level'],
  })
  .send();
Enter fullscreen mode Exit fullscreen mode

删除项目命令

从选项方面来看,该DeleteItem命令基本上是PutItemGetItem命令的混合体:

import { DeleteItemCommand } from 'dynamodb-toolbox';

const { Attributes } = await pokemonEntity
  .build(DeleteItemCommand)
  .key(pokemonKey)
  .options({
    capacity: 'TOTAL',
    metrics: 'SIZE',
    // 👇 Will type the response `Attributes`
    returnValues: 'ALL_OLD',
    condition: {
      or: [
        { attr: 'level', lte: 99 },
        ...
      ],
    },
  })
  .send();
Enter fullscreen mode Exit fullscreen mode

更新物品命令

UpdateCommand是所有 DynamoDB 命令中最丰富的一个。它的选项与以下选项类似PutItemCommand

import { UpdateItemCommand } from 'dynamodb-toolbox';

const { Item } = await pokemonEntity
  .build(UpdateItemCommand)
  .item(pokemonItem)
  .options({
    capacity: 'TOTAL',
    metrics: 'SIZE',
    // 👇 Will type the response `Attributes`
    returnValues: 'ALL_NEW',
    condition: {
      or: [
        { attr: 'level', lte: 99 },
        ...
      ],
    },
  })
  .send();
Enter fullscreen mode Exit fullscreen mode

然而,它的item方法还提供了更多可能性🙌 让我们一起来探索吧:

删除属性

可以使用该实用程序删除任何可选属性$remove

import { $remove } from 'dynamodb-toolbox';

const pokemonSchema = schema({
  ...
  isLegendary: boolean().optional(),
});

pokemonEntity.build(UpdateItemCommand).item({
  ...
  isLegendary: $remove(),
});
Enter fullscreen mode Exit fullscreen mode

引用已保存的值

您可以使用以下$get工具引用已保存的属性值:

import { $get } from 'dynamodb-toolbox';

pokemonEntity.build(UpdateItemCommand).item({
  ...
  // 👇 Resolved by DynamoDB at write time
  previousLevel: $get('level'),
});
Enter fullscreen mode Exit fullscreen mode

允许自引用。如果指定的属性路径在已保存的项中不存在,您还可以提供一个备用值作为第二个参数:

pokemonEntity.build(UpdateItemCommand).item({
  ...
  previousLevel: $get('level', 1),
  // 👇 fallback can also be a reference!
  chainedRefs: $get(
    'firstRef',
    $get('secondRef', 'Sky is the limit!'),
  ),
});
Enter fullscreen mode Exit fullscreen mode

请注意,属性路径会进行类型检查,但其属性值是否扩展了更新后的属性值目前尚不清楚,因此请格外小心:

const pokemonSchema = schema({
  ...
  name: string(),
  level: number(),
});

pokemonEntity.build(UpdateItemCommand).item({
  // ❌ Will be caught
  name: $get('non.existing[0].attribute'),
  // 🙈 Will NOT be caught
  level: $get('name'),
});
Enter fullscreen mode Exit fullscreen mode

非递归属性

对于非递归属性(例如基本类型)sets,更新将完全覆盖其先前的值:

const pokemonSchema = schema({
  ...
  isLegendary: boolean(),
  level: number(),
  name: string(),
  binEncoded: binary(),
  skills: set(string()),
});

pokemonEntity.build(UpdateItemCommand)
  .item({
    ...
    // 👇 Set fields to desired values
    isLegendary: true,
    nextLevel: 42,
    name: 'Pikachu',
    binEncoded: Buffer.from(...),
    skills: new Set(['thunder'])
  })
Enter fullscreen mode Exit fullscreen mode

number属性可以从额外的$sum$subtract$add操作中受益,这些操作可以使用引用:

import { $add, $subtract, $get } from 'dynamodb-toolbox';

await pokemonEntity.build(UpdateItemCommand)
  .item({
    ...
    health: $subtract($get('health'), 20),
    level: $sum($get('level', 0), 1),
    // 👇 Similar to
    level: $add(1),
  })
  .send();
Enter fullscreen mode Exit fullscreen mode

要从集合中添加或删除特定值,可以使用$add以下$delete工具:

pokemonEntity.build(UpdateItemCommand)
  .item({
    ...
    skills: $add('thunder', 'dragon-tail'),
    types: $delete('flight'),
  })
Enter fullscreen mode Exit fullscreen mode

递归属性

对于递归属性(例如 ` listsa`mapsrecords`b`),默认情况下更新是部分更新。您可以使用 `--override` 参数$set来指定完全覆盖:

const pokemonSchema = schema({
  ...
  types: list(string()),
  skills: list(string()),
  some: map({
    nested: map({
      field: string(),
      otherField: number(),
    }),
  }),
  bestSkillByType: record(string(), string()),
});

// 👇 Partial overrides
pokemonEntity.build(UpdateItemCommand).item({
  ...
  // 👇 Indexes 0 and 2 will be updated
  skills: ['thunder', undefined, $remove()],
  // 👇 Similar to
  skills: {
    0: 'thunder',
    2: $remove(),
  },
  some: {
    nested: {
      field: 'foo',
    },
  },
  bestSkillByType: {
    electric: 'thunder',
    flight: $remove(),
  },
});

import { $set } from 'dynamodb-toolbox';

// 👇 Complete overrides
pokemonEntity.build(UpdateItemCommand).item({
  ...
  skills: $set(['thunder']),
  some: $set({
    nested: {
      field: 'foo',
      otherField: 42,
    },
  }),
  bestSkillByType: $set({
    electric: 'thunder',
  }),
});
Enter fullscreen mode Exit fullscreen mode

lists属性可以从额外的操作中受益$append$prepend这些操作可以使用引用:

pokemonEntity.build(UpdateItemCommand).item({
  ...
  skills: $append(['thunder', 'dragon-tail']),
  levelHistory: $append($get('level')),
  types: $prepend(['flight']),
});
Enter fullscreen mode Exit fullscreen mode

任何属性和 anyOf 属性

any属性支持上述所有语法。anyOf目前尚不支持属性更新。

在测试中模拟实体

虽然我很欣赏这种链式语法,但它使得在单元测试中进行模拟变得困难。因此,我们v1提供了一个mockEntity实用工具来帮助您模拟命令:

import { mockEntity } from 'dynamodb-toolbox';

const mockedPokemonEntity = mockEntity(pokemonEntity);

mockedPokemonEntity.on(GetItemCommand).resolve({
  // 🙌 Type-safe!
  Item: {
    pokemonId: 'pikachu1',
    name: 'Pikachu',
    level: 42,
    ...
  },
});

// 👇 For more fine-grained control
mockedPokemonEntity
  .on(GetItemCommand)
  .mockImplementation((key, options) => ({
    // 🙌 Still type-safe!
    Item: {
      pokemonId: 'pikachu1',
      ...
    },
  }));

//👇 To simulate errors
mockedPokemonEntity.on(GetItemCommand).reject('Something bad happened');
Enter fullscreen mode Exit fullscreen mode

然后,您可以对接收到的命令进行断言:

await pokemonEntity
  .build(GetItemCommand)
  .key({ pokemonId: 'pikachu1' })
  .options({ consistent: true })
  .send();
// => Will return mocked values!

mockedPokemonEntity.received(GetItemCommand).count();
// => 1
mockedPokemonEntity.received(GetItemCommand).args(0);
// => [{ pokemonId: 'pikachu1' }, { consistent: true }]
mockedEntity.received(GetItemCommand).allArgs();
// => [[{ pokemonId: 'pikachu1' }, { consistent: true }], ...anyOtherCall]
Enter fullscreen mode Exit fullscreen mode

实用助手和类型

除了 ` SavedItemand`FormattedItem类型之外,`the`v1还公开了一系列有用的辅助函数和实用类型:

formatSavedItem

formatSavedItem将 DynamoDB 客户端返回的已保存项转换为其格式化的对应项:

import { formatSavedItem } from 'dynamodb-toolbox';

// 🙌 Typed as FormattedItem<typeof pokemonEntity>
const formattedPokemon = formatSavedItem(
  pokemonEntity,
  savedPokemon,
  // As in GetItem commands, attributes will filter the formatted item
  { attributes: [...] },
);
Enter fullscreen mode Exit fullscreen mode

请注意,这是一个解析操作,也就是说,它不要求项目必须按指定类型输入SavedItem<typeof myEntity>,但如果保存的项目无效,则会抛出错误:

const formattedPokemon = formatSavedItem(pokemonEntity, {
  ...
  level: 'not a number',
});
// ❌ Will raise error:
// => "Invalid attribute in saved item: level. Should be a number"
Enter fullscreen mode Exit fullscreen mode

条件和解析条件

typeConditionparseConditionutil 函数可用于对条件进行类型定义和构建条件表达式:

import { Condition, parseCondition } from 'dynamodb-toolbox';

const condition: Condition<typeof pokemonEntity> = {
  attr: 'level',
  lte: 42,
};

const parsedCondition = parseCondition(pokemonEntity, condition);
// => {
//   ConditionExpression: "#c1 <= :c1",
//   ExpressionAttributeNames: { "#c1": "level" },
//   ExpressionAttributeValues: { ":c1": 42 },
// }
Enter fullscreen mode Exit fullscreen mode

投影和解析投影

typeAnyAttributePathparseProjectionutil 函数可用于对属性路径进行类型定义和构建投影表达式:

import { AnyAttributePath, parseProjection } from 'dynamodb-toolbox';

const attributes: AnyAttributePath<typeof pokemonEntity>[] = [
  'pokemonType',
  'levelHistory.currentLevel',
];

const parsedProjection = parseProjection(pokemonEntity, attributes);
// => {
//   ProjectionExpression: '#p1, #p2.#p3',
//   ExpressionAttributeNames: {
//     '#p1': 'pokemonType',
//     '#p2': 'levelHistory',
//     '#p3': 'currentLevel',
//   },
// }
Enter fullscreen mode Exit fullscreen mode

键输入和主键

这两种类型都可用于输入项目主键:

import type { KeyInput, PrimaryKey } from 'dynamodb-toolbox';

type PokemonKeyInput = KeyInput<typeof pokemonEntity>;
// => { pokemonClass: string, pokemonId: string }

type MyTablePrimaryKey = PrimaryKey<typeof myTable>;
// => { PK: string, SK: string }
Enter fullscreen mode Exit fullscreen mode

错误

最后,我们快速看一下错误管理。当 DynamoDB-Toolbox 遇到意外输入时,它会抛出一个实例DynamoDBToolboxError,该实例本身扩展了原生Error类并添加了一个code属性:

await pokemonEntity
  .build(PutItemCommand)
  .item({ ..., level: 'not a number' })
  .send();
// ❌ [parsing.invalidAttributeInput] Attribute level should be a number
Enter fullscreen mode Exit fullscreen mode

有些类型DynamoDBToolboxErrors还会暴露一个path属性(主要用于验证)和/或一个payload用于提供额外上下文的属性。如果需要处理这些类型,TypeScript 是你的最佳选择,因为该code属性能够正确区分DynamoDBToolboxError类型:

import { DynamoDBToolboxError } from 'dynamodb-toolbox';

const handleError = (error: Error) => {
  if (!error instanceof DynamoDBToolboxError) throw error;

  switch (error.code) {
    case 'parsing.invalidAttributeInput':
      const path = error.path;
      // => "level"
      const payload = error.payload;
      // => { received: "not a number", expected: "number" }
      break;
      ...
    case 'entity.invalidItemSchema':
      const path = error.path; // ❌ error does not have path property
      const payload = error.payload; // ❌ same goes with payload
      ...
  }
};
Enter fullscreen mode Exit fullscreen mode

结论

就先说到这里啦!希望你们和我一样期待这次的新发布🙌

如果您有我遗漏的功能建议,或者希望我提到的某些功能优先考虑,请在本文下方留言,或在官方代码仓库中创建 issue 或发起讨论,并添加v1👍 标签。

再见!

文章来源:https://dev.to/slsbytheodo/updated-the-dynamodb-toolbox-v1-beta-is-here-all-you-need-to-know-ep2