【更新】DynamoDB-Toolbox v1 beta 版上线啦🙌 你需要知道的一切!
☝️注意:本文是关于版本
beta.1发布的。👉如果您需要有关该
beta.0版本的文档,您可能需要查找本文的先前版本。👉如果您想从 迁移
beta.0到beta.1,有一篇专门的文章总结了这些变化。
在Theodo ,我们都是 Jeremy Daly 的DynamoDB Toolbox的忠实粉丝。我们早在 2019 年就开始使用它,并且越来越喜欢它……但我们也很清楚它的不足之处😅
其中一个原因是它最初是用 JavaScript 编写的。虽然 Jeremy 在 2020 年用 TypeScript 重写了源代码,但它没有处理类型推断,而我最终在v0.4版本中自己实现了这个功能。
然而,我们仍然觉得缺少一些功能:从声明enums原始类型,到支持递归模式和类型(列表和映射子属性)以及多态性。
我也曾对面向对象的方法有所顾虑:我并不反对类,但它们无法进行摇树优化(tree-shaking)。这意味着在无服务器环境中,它们应该保持相对轻量级。AWS 在其 SDK v3中就采用了这种方法,而且理由充分:保持包的精简!
但 DynamoDB-Toolbox 的情况并非如此:我记得我曾经编写过一个.update超过 1000 行的方法……但为什么要把它打包在一起,而你根本不需要它呢?
因此,去年我决定全身心投入到代码的彻底改造中,主要目标有三个:
- 支持 AWS SDK v3 版本(尽管v0.8 版本已经添加了支持)。
- 使 API 和类型推断与zod和electrodb等更“现代”的工具相媲美。
- 采用更具函数性和可进行摇树优化的方法
今天,我很高兴地宣布dynamodb-toolbox v1 beta 版发布🙌 它包含了重新设计的Table类Entity,以及对 ` 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
该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
表格
表格的定义方式与之前的版本基本相同,但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,
});
☝️ v1 版本尚不支持索引,因为查询功能尚未推出。
可以通过 getter 方法提供表名,这在某些情况下非常有用,例如在不实际运行任何命令的情况下使用该类(例如测试或部署):
const myTable = new TableV2({
...
// 👇 Only executed at command execution
name: () => process.env.TABLE_NAME,
});
与之前的版本一样,这些类会通过一个内部字符串属性(默认保存为 ` <object_id> v1`)为数据添加实体标识符。可以通过参数在类级别重命名该属性:entity"_et"TableentityAttributeSavedAs
const myTable = new TableV2({
...
// 👇 defaults to "_et"
entityAttributeSavedAs: '__entity__',
});
实体
对于实体而言,主要变化在于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({ ... }),
});
时间戳
内部时间戳属性也存在,其行为与之前的版本类似。您可以设置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__',
},
},
});
匹配表架构
与之前的版本相比,一个重要的变化是,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'),
...
}),
});
- 如果实体键属性与表架构不匹配,则该类
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,
}),
});
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,
// }
设计实体模式
现在让我们深入了解一下改动最大的部分:模式定义。
模式定义
与zod或yup类似,属性现在通过函数构建器定义。对于 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();
在被声明包裹之前schema,属性被称为“温属性”:它们未经(运行时)验证,可用于构建其他模式。通过检查它们的类型,你会发现它们带有前缀$。一旦被冻结,就会应用验证并移除构建方法。
主要结论是,热模式可以组合,而冻结模式则不能:
import { schema } from 'dynamodb-toolbox';
const pokemonName = string();
const pokemonSchema = schema({
// 👍 No problem
pokemonName,
...
});
const pokedexSchema = schema({
// ❌ Not possible
pokemon: pokemonSchema,
...
});
您可以使用专用方法或提供选项对象来创建/更新预配置属性。前者提供了一个简洁的 devX 界面,支持自动完成和简写,而后者理论上需要的计算时间和内存使用量更少,尽管实际影响应该非常小(验证仅在冻结时应用):
// Using methods
const pokemonName = string().required('always');
// Using options
const pokemonName = string({ required: 'always' });
所有属性都具有以下选项:
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' });
与之前的版本相比,一个非常重要的重大变更在于,现在默认情况下必须包含根属性和 Map 子属性。这样做是为了更好地协同工作,实现组合和验证功能。
💡除了根属性和 Map 子属性(例如字符串列表)之外,子模式不应该是可选的。所以,我应该强制用户
list(string().required())每次都写子模式,还是应该让字符串验证和类型推断能够感知上下文(在列表中忽略子模式,但在 Map 中保留)?我觉得默认强制要求子模式并阻止类似这样的模式required会更优雅。string()list(string().optional())
hidden(boolean?=true)格式化命令返回项时跳过该属性:
const pokemonName = string().hidden();
const pokemonName = string({ hidden: true });
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' });
savedAs(字符串)以前称为map。在发送命令之前重命名根属性或 Map 子属性:
const pokemonName = string().savedAs('_n');
const pokemonName = string({ savedAs: '_n' });
default(计算默认值)请参阅“计算默认值”
属性类型
以下是所有可用属性类型的完整列表:
任何
定义一个任意值的属性。运行时不会进行任何验证,其类型将解析为unknown:
import { any } from 'dynamodb-toolbox';
const pokemonSchema = schema({
...
metadata: any(),
});
type FormattedPokemon = FormattedItem<typeof pokemonEntity>;
// => {
// ...
// metadata: unknown
// }
您可以通过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!');
基本元素
定义一个、string或number属性: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
// }
与属性类似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,
},
});
原始类型还有一个额外的enum选项。例如,您可以提供一个有限的宝可梦类型列表:
const pokemonTypeAttribute = string().enum('fire', 'grass', 'water');
// Shorthand for `.enum("POKEMON").default("POKEMON")`
const pokemonPartitionKey = string().const('POKEMON');
💡出于类型推断的考虑,该
enum选项仅作为方法可用,不作为对象选项可用。
放
定义一组字符串、数字或二进制数据。与之前的版本不同,集合现在以Set类的形式存在。如果您更倾向于使用数组(或者希望两者都能使用),请告诉我:
import { set } from 'dynamodb-toolbox';
const pokemonSchema = schema({
...
skills: set(string()),
});
type FormattedPokemon = FormattedItem<typeof pokemonEntity>;
// => {
// ...
// skills: Set<string>
// }
选项可以作为第二个参数提供:
const setAttr = set(string()).hidden();
const setAttr = set(string(), { hidden: true });
列表
定义任意类型的子模式列表:
import { list } from 'dynamodb-toolbox';
const pokemonSchema = schema({
...
skills: list(string()),
});
type FormattedPokemon = FormattedItem<typeof pokemonEntity>;
// => {
// ...
// skills: string[]
// }
与集合一样,选项可以作为第二个参数提供。
地图
定义一个有限的键值对列表。键必须遵循字符串模式,而值可以是任何类型的子模式:
import { map } from 'dynamodb-toolbox';
const pokemonSchema = schema({
...
nestedMagic: map({
will: map({
work: string().const('!'),
}),
}),
});
type FormattedPokemon = FormattedItem<typeof pokemonEntity>;
// => {
// ...
// nestedMagic: {
// will: {
// work: "!"
// }
// }
// }
与集合和列表一样,选项可以作为第二个参数提供。
记录
记录( 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
// }
// }
选项可以作为第三个参数提供:
const recordAttr = record(string(), number()).hidden();
const recordAttr = record(string(), number(), { hidden: true });
任何
一种新的元属性类型,表示类型的并集,即一系列可能的类型:
import { anyOf } from 'dynamodb-toolbox';
const pokemonSchema = schema({
...
pokemonType: anyOf([
string().const('fire'),
string().const('grass'),
string().const('water'),
]),
});
在这个特定情况下,`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 }
与集合、列表和映射一样,选项可以作为第二个参数提供。
期待
暂时就这些!我计划在某个时候添加新null的tuple属性。allOf
如果您希望看到其他类型,欢迎在本文章下方留言,或在官方代码仓库中发起讨论并添加v1👍 标签。
计算出的默认值
在之前的版本中,default该功能用于根据其他属性值计算属性值。对于复合索引等“技术性”属性,此功能非常实用。
然而,在 TypeScript 中正确输入代码是不可能的:
const pokemonSchema = schema({
...
level: number(),
levelPlusOne: number().default(
// ❌ No way to retrieve the caller context
input => input.level + 1,
),
});
这意味着input输入的内容被随意输入,而正确输入则成了开发人员的责任,这对我来说是无法接受的。
我最终采用的解决方案是将计算默认值的声明拆分为两个步骤:
- 首先,声明属性默认值应派生自其他属性:
import { ComputedDefault } from 'dynamodb-toolbox';
const pokemonSchema = schema({
...
level: number(),
levelPlusOne: number().default(ComputedDefault),
});
💡
ComputedDefault是一个 JavaScript符号(TLDR:一种独特且自定义的符号null),因此它不可能与实际所需的默认值冲突。
- 然后,通过属性声明一种在实体级别计算此属性的方法
putDefaults:
const pokemonEntity = new EntityV2({
...
schema: pokemonSchema,
putDefaults: {
// 🙌 Correctly typed!
levelPlusOne: ({ level }) => level + 1,
},
});
- 同样的情况也适用于
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,
},
});
在嵌套属性这种棘手的情况下,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,
},
},
},
});
命令
既然我们已经了解了如何设计实体,接下来让我们看看如何利用它们来编写命令👍
💡测试版仅支持 `
PutItem、`GetItem、`UpdateItem和DeleteItem` 命令。如果您需要运行`Query或Scan` 命令,我的建议是运行原生 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();
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();
您还可以使用.build实体的方法来直接构造一个与您的实体关联的命令:
// 🙌 We get a syntax closer to v0.x... but tree-shakable!
const response = await pokemonEntity
.build(PutItemCommand)
.item(pokemonItem)
.options(options)
.send();
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();
获取物品命令
该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();
删除项目命令
从选项方面来看,该DeleteItem命令基本上是PutItem和GetItem命令的混合体:
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();
更新物品命令
这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();
然而,它的item方法还提供了更多可能性🙌 让我们一起来探索吧:
删除属性
可以使用该实用程序删除任何可选属性$remove:
import { $remove } from 'dynamodb-toolbox';
const pokemonSchema = schema({
...
isLegendary: boolean().optional(),
});
pokemonEntity.build(UpdateItemCommand).item({
...
isLegendary: $remove(),
});
引用已保存的值
您可以使用以下$get工具引用已保存的属性值:
import { $get } from 'dynamodb-toolbox';
pokemonEntity.build(UpdateItemCommand).item({
...
// 👇 Resolved by DynamoDB at write time
previousLevel: $get('level'),
});
允许自引用。如果指定的属性路径在已保存的项中不存在,您还可以提供一个备用值作为第二个参数:
pokemonEntity.build(UpdateItemCommand).item({
...
previousLevel: $get('level', 1),
// 👇 fallback can also be a reference!
chainedRefs: $get(
'firstRef',
$get('secondRef', 'Sky is the limit!'),
),
});
请注意,属性路径会进行类型检查,但其属性值是否扩展了更新后的属性值目前尚不清楚,因此请格外小心:
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'),
});
非递归属性
对于非递归属性(例如基本类型)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'])
})
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();
要从集合中添加或删除特定值,可以使用$add以下$delete工具:
pokemonEntity.build(UpdateItemCommand)
.item({
...
skills: $add('thunder', 'dragon-tail'),
types: $delete('flight'),
})
递归属性
对于递归属性(例如 ` listsa`maps和records`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',
}),
});
lists属性可以从额外的操作中受益$append,$prepend这些操作可以使用引用:
pokemonEntity.build(UpdateItemCommand).item({
...
skills: $append(['thunder', 'dragon-tail']),
levelHistory: $append($get('level')),
types: $prepend(['flight']),
});
任何属性和 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');
然后,您可以对接收到的命令进行断言:
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]
实用助手和类型
除了 ` 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: [...] },
);
请注意,这是一个解析操作,也就是说,它不要求项目必须按指定类型输入SavedItem<typeof myEntity>,但如果保存的项目无效,则会抛出错误:
const formattedPokemon = formatSavedItem(pokemonEntity, {
...
level: 'not a number',
});
// ❌ Will raise error:
// => "Invalid attribute in saved item: level. Should be a number"
条件和解析条件
typeCondition和parseConditionutil 函数可用于对条件进行类型定义和构建条件表达式:
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 },
// }
投影和解析投影
typeAnyAttributePath和parseProjectionutil 函数可用于对属性路径进行类型定义和构建投影表达式:
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',
// },
// }
键输入和主键
这两种类型都可用于输入项目主键:
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 }
错误
最后,我们快速看一下错误管理。当 DynamoDB-Toolbox 遇到意外输入时,它会抛出一个实例DynamoDBToolboxError,该实例本身扩展了原生Error类并添加了一个code属性:
await pokemonEntity
.build(PutItemCommand)
.item({ ..., level: 'not a number' })
.send();
// ❌ [parsing.invalidAttributeInput] Attribute level should be a number
有些类型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
...
}
};
结论
就先说到这里啦!希望你们和我一样期待这次的新发布🙌
如果您有我遗漏的功能建议,或者希望我提到的某些功能优先考虑,请在本文下方留言,或在官方代码仓库中创建 issue 或发起讨论,并添加v1👍 标签。
再见!
文章来源:https://dev.to/slsbytheodo/updated-the-dynamodb-toolbox-v1-beta-is-here-all-you-need-to-know-ep2
