# @faceteer/facet 中文文档

规范地址：https://npmcn.com/package/%40faceteer/facet/

版本：6\.1\.0
npm 元数据更新时间：2026-08-22T15:52:04.067Z
中文译文更新时间：2026-11-14T08:12:57.014Z
软件包许可：MIT

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40faceteer/facet
原文对照：https://npmcn.com/package/%40faceteer/facet/?lang=original
项目主页：https://github.com/faceteer/facet#readme
源码仓库：https://github.com/faceteer/facet

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：125 kB，41 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=22\.12\.0
- 运行依赖：0 个
- peer 依赖：@aws-sdk/client-dynamodb ^3\.0\.0
- 首次发布：2021-07-10T05:03:21.524Z
- 最新版本发布：2026-08-22T15:52:03.712Z
- 近 30 天下载量：3572（2026-10-14 至 2026-11-12，npm 官方下载统计）
- 已发布版本数：19（近 12 个月 3 个）

## 中文简介

![Faceteer](https://imagedelivery.net/zms_mS5SEgxiPTJtX-M2fQ/e83e9689-bace-449d-86b4-cccb45d65700/public)

## 安装

```sh
npm install @faceteer/facet
```

也可以使用：`pnpm add @faceteer/facet`、`yarn add @faceteer/facet`、`bun add @faceteer/facet`

## README

![Faceteer](https://imagedelivery.net/zms_mS5SEgxiPTJtX-M2fQ/e83e9689-bace-449d-86b4-cccb45d65700/public)

[![codecov](https://codecov.io/gh/faceteer/facet/branch/main/graph/badge.svg?token=ORKKZWL5N6)](https://codecov.io/gh/faceteer/facet) [![Test](https://github.com/faceteer/facet/actions/workflows/test.yml/badge.svg)](https://github.com/faceteer/facet/actions/workflows/test.yml) [![npm version](https://img.shields.io/npm/v/@faceteer/facet)](https://www.npmjs.com/package/@faceteer/facet)

Faceteer的用意是让单表DynamotB架构更容易工作.

保存在Dynamo DB表格中的结构和记录更是围绕你如何访问数据而不是数据本身的结构来设计.

如果你不熟悉Dynamo DB单表概念,这段视频是一个很好的起点.

[亚马逊迪纳摩DB深潜](https://www.youtube.com/watch?v=6yqfmXiZTlM)

## 开始

本文件旨在温和地介绍该框架及其特点.

我们将创建一个模拟"任务"应用程序.

### 安装

用 npm 安装 . `@aws-sdk/client-dynamodb` 是一个对等依赖,所以同时安装:

```
npm i @faceteer/facet @aws-sdk/client-dynamodb --save
```

Faceteer需要22.12节节点或更新.

### 访问模式

了解和识别您的应用访问模式对于单表设计至关重要. Faceteer不适合进行临时查询.

以我们为例,我们将建立一个应用程序,以跟踪一组人的任务.

我们的应用程序应当支持多个团队,团队应当有多个用户,团队任务或者未指定,或者分配给个人用户.

我们需要支持的准入模式是:

-   _"找一个团队的ID"_
-   _"用他们的身份证明"_
-   _"通过他们的电子邮件地址"_
-   _"用它的身份来完成任务"_
-   _"按创建日期订购的团队列表用户"_
-   _"由团队下令的列表任务 日期创建"_
-   _"按规定日期排队"_
-   _"按用户和按创建日期顺序排列的列表任务"_
-   _"按用户和按日期顺序排列的任务列表"_

以下是我们将用于我们的应用程序的类型:

```ts
export interface Team {
    teamId: string;
    teamName: string;
    dateCreated: Date;
    dateDeleted?: Date;
}

export interface User {
    userId: string;
    teamId: string;
    email: string;
    password: string;
    dateCreated: Date;
    dateDeleted?: Date;
}

export interface Task {
    taskId: string;
    teamId: string;
    assignedUserId?: string;
    dateCreated: Date;
    dateDue: Date;
    status: 'open' | 'completed' | 'deleted';
}
```

### 验证

Faceteer要求每个面都有一个验证器功能. 默认它运行 **读取**,因此无效的记录永远无法到达您的应用程序代码。 该函数采用未知的输入并返回一个与平面模型相匹配的对象,或者如果输入无效则丢出.

如果您还需要验证 **写入**,通过 `validateInput: true` 到侧面构造器。 这增加了每个电话费用,但默认取消.

```ts
import AJV, { JSONSchemaType } from "ajv";

export interface Team {
  teamId: string;
  teamName: string;
  dateCreated: Date;
  dateDeleted?: Date;
}

// Here we use AJV to define the schema for a team
const schema: JSONSchemaType<Team> = {
  type: "object",
  additionalProperties: false,
  properties: {
    teamId: { type: "string" },
    teamName: { type: "string" },
    dateCreated: { type: "object", format: "date-time" },
    dateDeleted: { type: "object", format: "date-time", nullable: true },
  },
  required: ["teamId", "teamName", "dateCreated"],
};
const validateTeam = ajv.compile(schema);

// We'll pass this function to our Facet for validating records from the DB
export function teamValidator(input: unknown): Team {
  if (validateTeam(input)) {
    return input;
  }
  throw validateTeam.errors[0];
}
...
```

### 创建面板

让我们来创造我们的层面! 由于我们所有的应用逻辑被团队隔离,我们将使用团队ID作为我们所有模型的分区.

每一面都需要一个独特的 `name`它写在每张记录下 `facet` 属性,使您在多个方面共享一个表格时区分项目类型.

```ts
import { Facet } from '@faceteer/facet';
import { teamValidator, userValidator, taskValidator } from './models';
import { DynamoDB } from '@aws-sdk/client-dynamodb';

const dynamoDbClient = new DynamoDB({});
const dynamoDbTableName = 'ExampleTableName';

// Facet containing our teams
const TeamFacet = new Facet({
    name: 'TEAM',
    PK: {
        keys: ['teamId'],
        prefix: '#TEAM',
    },
    SK: {
        keys: ['teamId'],
        prefix: '#TEAM',
    },
    connection: {
        dynamoDb: dynamoDbClient,
        tableName: dynamoDbTableName,
    },
    validator: teamValidator,
});

// Facet containing our users
const UserFacet = new Facet({
    name: 'USER',
    PK: {
        keys: ['teamId'],
        prefix: '#TEAM',
    },
    SK: {
        keys: ['userId'],
        prefix: '#USER',
    },
    connection: {
        dynamoDb: dynamoDbClient,
        tableName: dynamoDbTableName,
    },
    validator: userValidator,
});

// Facet containing our tasks
const TaskFacet = new Facet({
    name: 'TASK',
    PK: {
        keys: ['teamId'],
        prefix: '#TEAM',
    },
    SK: {
        keys: ['taskId'],
        prefix: '#TASK',
    },
    connection: {
        dynamoDb: dynamoDbClient,
        tableName: dynamoDbTableName,
    },
    validator: taskValidator,
});
```

### 记录

现在我们可以把一个团队插入我们的桌子.

```ts
import { nanoid } from 'nanoid';
import { TeamFacet } from './facets';
import type { Team } from './models';

const team: Team = {
    teamId: nanoid(),
    teamName: 'Penguin Popsicles',
    dateCreated: new Date(),
};

const putResult = await TeamFacet.put(team);

if (putResult.wasSuccessful) {
    // `record` will contain the record that was
    // put into the database
    return putResult.record;
} else {
    // If there is an issue putting the record into
    // the database the error will be attached to `error`
    throw putResult.error;
}
```

我们还可以同时放几个唱片. Faceteer 块阵列将25个物品的最大批量大小放入DynamomDB,并重试任何 `UnprocessedItems` 最多5倍的回转指数 复试后仍然失败的记录在 `failed`。 。 。 .

让我们做一个功能来创建多个用户.

```ts
import { nanoid } from 'nanoid';
import { UserFacet } from './facets';
import type { User } from './models';

export async function createNewUsers(usersToCreate: Omit<User, 'userId'>[]) {
    // Generate new unique IDs for users
    const users: User[] = usersToCreate.map((userToCreate) => ({
        userId: nanoid(),
        ...userToCreate,
    }));

    // Save the new users to Dynamo DB
    const putResult = await UserFacet.put(users);

    // Check to see if any of the put requests failed
    if (putResult.hasFailures) {
        for (const putFailure of putResult.failed) {
            handleFailure(putFailure);
        }
    }

    // Return the users that were successfully created
    return putResult.put;
}
```

#### 有条件的地方

传出 a `condition` 单项 `put` 防止写作会破坏现有数据。 条件包括字段名称的翻转、运算符和运算符的操作:参照符(`=`, (中文(简体) ), `<>`, (中文(简体) ), `<`, (中文(简体) ), `<=`, (中文(简体) ), `>`, (中文(简体) ), `>=`, (中文(简体) ), `between`, (中文(简体) ), `begins_with`, (中文(简体) ), `contains`, (中文(简体) ), `size`, (中文(简体) ), `in`, (中文(简体) ), `exists`,以及 `not_exists`的表达式 `[left, 'AND' | 'OR', right]` 双对和 `{ NOT: expression }`。 。 。 .

```ts
// Only create the user if one with this partition/sort key doesn't already exist
const result = await UserFacet.put(user, {
    condition: ['userId', 'not_exists'],
});
```

`condition` 仅适用于单个项目的呼叫。 如果你通过它与一个阵列, 它被默默地忽略.

### 获取记录

从 Facet 获取记录需要分区键中使用的任何属性 _和_ 排序键.

这是因为Dynamo DB通过两个密钥的组合在表格中独有地识别记录.

```ts
import { UserFacet } from './facets';
import type { User } from './models';

export async function getUser(teamId: string, userId: string) {
    // Since `teamId` and `userId` are used by the UserFacet to
    // create the partition and sort keys, both must be provided
    // to get the user record
    const user = await UserFacet.get({ teamId, userId });

    // If there is no record matching the combined partition and sort
    // keys faceteer will return null
    if (!user) {
        throw new Error('User not found');
    }
    return user;
}
```

得到的请求总是 返回一个准确的记录,或 `null` 数值.

也可以通过一系列识别符来获取一个呼叫中的许多记录. Faceteer将请求分解到DynamoDB最大批量的100个项目,并重试任何 `UnprocessedKeys` 最多10倍 与指数反射。 返回的数组无法保证保存输入顺序 .

```ts
const users = await UserFacet.get([
    { teamId, userId: 'a' },
    { teamId, userId: 'b' },
    { teamId, userId: 'c' },
]);
```

### 查询

查询记录分两部分进行.

首先,您必须指定您要查询的分区 .

```ts
import { UserFacet } from './facets';

export async function getUsersForTeam(teamId: string) {
    const partition = UserFacet.query({ teamId });
}
```

然后指定您要在分区上运行的查询操作 .

```ts
import { UserFacet } from './facets';

export async function getUsersForTeam(teamId: string) {
    // You can store a partition in a variable to re-use it
    const teamPartition = UserFacet.query({ teamId });
    const { records, cursor } = await teamPartition.list();

    // Or you can call an operation on a partition directly
    const { records, cursor } = await UserFacet.query({ teamId }).list();

    return { users: records, cursor };
}
```

您可以通过下列方式查询:

-   `equals(key)`
    
    -   以完全符合指定排序键的排序键返回记录.
-   `greaterThan(key)`
    
    -   以大于指定排序键的排序键返回记录.
-   `greaterThanOrEqual(key)`
    
    -   以大于或等于指定排序键的排序键返回记录.
-   `lessThan(key)`
    
    -   以小于指定排序键的排序键返回记录.
-   `lessThanOrEqual(key)`
    
    -   以小于或等于指定排序键的排序键返回记录.
-   `beginsWith(key)`
    
    -   以排序键返回以指定排序键开头的记录.
-   `between(startKey, endKey)`
    
    -   以比起键大或等同的排序键返回记录,并小于或等同起键.
-   `list()`
    
    -   返回排序键以该排序键的正面前缀开头的记录
-   `first()`
    
    -   相当于打电话 `list()` 选择第一个选项

结果总是由排序键排序 .

每个查询操作员接受共享 `options` 您可以通过一个参数 `filter`页:1 `limit`页:1 `cursor` 给帕吉纳, `scanForward: false` 以反向顺序,并且(对于被压缩的密钥) a `shard` 编号。 过滤器使用与 [条件设置](https://npmcn.com/package/%40faceteer/facet/#readme-conditional-puts) (仅限于比较者, `between`,以及 `begins_with`),并在关键条件后运行在服务器上,因此它们缩小了响应而不是读取成本.

```ts
// Everything in the partition except failed tasks
const { records, cursor } = await TaskFacet.query({ teamId }).list({
    filter: ['status', '<>', 'failed'],
    limit: 50,
});
```

我们的进入模式之一是 _"按创建日期订购的团队列表用户"_,但我们的类密钥是由 `userId`。 。 。 .

一个选择是把用户的 `dateCreated` 作为用户排序密钥的一部分.

```diff
const UserFacet = new Facet({
  name: "USER",
  PK: {
    keys: ["teamId"],
    prefix: "#TEAM",
  },
  SK: {
-    keys: ["userId"],
+    keys: ["dateCreated", "userId"],
    prefix: "#USER",
  },
  connection: {
    dynamoDb: dynamoDbClient,
    tableName: dynamoDbTableName,
  },
  validator: userValidator,
});
```

但是,在使用这种方法之前,我们有一些限制需要考虑.

1.  分区和排序键为 **不可忽视** 无法改变.
2.  你个 **必须** 提供所有构成分区的属性, 并排序密钥, 当它得到一个记录的ID.

用户创建的日期不太可能改变,所以如果它是不可变的分区键的一部分,那就很好了. 将创建的日期作为用户主要标识的一部分, 似乎很奇怪, 一般来说应该是一种味道, 我们做错了什么.

第二项限制对我们来说是个问题 需要提供用户每次想要用他们的ID来获得该用户创建的日期将是很麻烦的.

另一个选项是每当创建用户时,用日期前缀用户的ID. 让我们重新访问我们 `createNewUsers()` 函数.

```diff
const users: User[] = usersToCreate.map((userToCreate) => ({
-  userId: nanoid(),
+  userId: userToCreate.dateCreated.getTime().toString(36) + nanoid(),
  ...userToCreate,
}));
```

现在 `userId` 将会从创建用户时的36个基本代表开始, 命令我们排序的密钥 !

虽然这是一个有用的技术, 在许多情况下,我们将无法修改 排序的密钥, 以便按我们的要求排列.

为了启用其它访问模式,我们经常需要使用索引.

### 索引

Dynamo DB使用"Global Second Index"(GSIs)来进行数据分取和调取入表. 这使得我们能增加我们各方面的准入模式.

让我们看看我们的任务需要的四种进入模式:

-   [ ] _"由团队下令的列表任务 日期创建"_
-   [ ] _"按规定日期排队"_
-   [ ] _"按用户和按创建日期顺序排列的列表任务"_
-   [ ] _"按用户和按日期顺序排列的任务列表"_

这就是我们的一面:

```ts
export const TaskFacet = new Facet({
    name: 'TASK',
    PK: {
        keys: ['teamId'],
        prefix: '#TEAM',
    },
    SK: {
        keys: ['taskId'],
        prefix: '#TASK',
    },
    connection: {
        dynamoDb: dynamoDbClient,
        tableName: dynamoDbTableName,
    },
    validator: taskValidator,
});
```

目前,我们只能列出由任务ID命令的团队的任务.

首先,我们可以使用有相继时间标识的技巧,允许任务 id 排序顺序匹配按排序生成的任务 。 这包含了我们的一个进入模式

-   [x] _"由团队下令的列表任务 日期创建"_
-   [ ] _"按规定日期排队"_
-   [ ] _"按用户和按创建日期顺序排列的列表任务"_
-   [ ] _"按用户和按日期顺序排列的任务列表"_

为了在规定日期前完成团队命令的任务,我们必须使用 `addIndex()` 命令.

```ts
import { Facet, Index } from '@faceteer/facet';

export const TaskFacet = new Facet({
    name: 'TASK',
    PK: {
        keys: ['teamId'],
        prefix: '#TEAM',
    },
    SK: {
        keys: ['taskId'],
        prefix: '#TASK',
    },
    connection: {
        dynamoDb: dynamoDbClient,
        tableName: dynamoDbTableName,
    },
    validator: taskValidator,
}).addIndex({
    index: Index.GSI1,
    PK: {
        keys: ['teamId'],
        prefix: '#TEAM',
    },
    SK: {
        keys: ['dateDue'],
        prefix: '#TASK_DUE',
    },
    alias: 'GSITeamDueDate',
});
```

现在我们可以用 `GSI1` 以查询我们的任务的预定日期!

```ts
export async function getTeamTasks(teamId: string) {
    const queryResult = await TaskFacet.GSI1.query({ teamId }).list();
    return {
        tasks: queryResult.records,
        cursor: queryResult.cursor,
    };
}
```

或者我们可以得到任何应该完成的任务.

```ts
export async function getPastDueTasks(teamId: string) {
    const today = new Date();
    const queryResult = await TaskFacet.GSI1.query({ teamId }).lessThan({
        dateDue: today,
    });
    return {
        tasks: queryResult.records,
        cursor: queryResult.cursor,
    };
}
```

我们也可以用别名 `GSITeamDueDate` 改为 `GSI1` 可读性.

```diff
export async function getPastDueTasks(teamId: string) {
    const today = new Date();
-	const queryResult = await TaskFacet.GSI1.query({ teamId }).lessThan({
+	const queryResult = await TaskFacet.GSITeamDueDate.query({ teamId }).lessThan({
        dateDue: today,
    });
    return {
        tasks: queryResult.records,
        cursor: queryResult.cursor,
    };
}

```

这使得另一种必要的进入模式成为可能.

-   [x] _"由团队下令的列表任务 日期创建"_
-   [x] _"按规定日期排队"_
-   [ ] _"按用户和按创建日期顺序排列的列表任务"_
-   [ ] _"按用户和按日期顺序排列的任务列表"_

后两个我们需要两个索引.

```ts
import { Facet, Index } from '@faceteer/facet';

export const TaskFacet = new Facet({
    name: 'TASK',
    PK: {
        keys: ['teamId'],
        prefix: '#TEAM',
    },
    SK: {
        keys: ['taskId'],
        prefix: '#TASK',
    },
    connection: {
        dynamoDb: dynamoDbClient,
        tableName: dynamoDbTableName,
    },
    validator: taskValidator,
})
    .addIndex({
        index: Index.GSI1,
        PK: {
            keys: ['teamId'],
            prefix: '#TEAM',
        },
        SK: {
            keys: ['dateDue'],
            prefix: '#TASK_DUE',
        },
        alias: 'GSITeamDueDate',
    })
    .addIndex({
        index: Index.GSI2,
        PK: {
            keys: ['assignedUserId', 'status'],
            prefix: '#USER_STATUS',
        },
        SK: {
            keys: ['dateCreated'],
            prefix: '#TASK_CREATED',
        },
        alias: 'GSIUserStatusCreated',
    })
    .addIndex({
        index: Index.GSI3,
        PK: {
            keys: ['assignedUserId', 'status'],
            prefix: '#USER_STATUS',
        },
        SK: {
            keys: ['dateDue'],
            prefix: '#TASK_DUE',
        },
        alias: 'GSIUserStatusDue',
    });
```

面对面者最多支持20个指数(`Index.GSI1` 结束 `Index.GSI20`),匹配了迪纳摩DB自己的每表GSI限制. 固定属性名称( E)`GSI1PK`编号 :`GSI1SK`, (中文(简体) ), `GSI2PK`编号 :`GSI2SK`,...)必须在您的桌面上宣布您实际使用的任何索引的计划.

-   [x] _"由团队下令的列表任务 日期创建"_
-   [x] _"按规定日期排队"_
-   [x] _"按用户和按创建日期顺序排列的列表任务"_
-   [x] _"按用户和按日期顺序排列的任务列表"_

### 预计为:

每个读取器返回一个记录,每个属性 DynamoDB存储. 当你只需要一个子集域时,你可以减少网络有效载荷和JSON-parse使用一个DynamoDB的成本 `ProjectionExpression`面对面者将它暴露为 `select` 选项.

预计为: `pickValidator` 在表面上。 全数 `validator` 因此,投影需要一个工厂,为任何选定的密钥子集生产次级验证器.

```ts
import { PickValidator } from '@faceteer/facet';

export const teamPickValidator: PickValidator<Team> = (keys) => {
    const mask: { [K in keyof Team]?: true } = {};
    for (const k of keys) mask[k as keyof Team] = true;
    const picked = teamSchema.pick(mask);
    return (input) => picked.parse(input) as Pick<Team, (typeof keys)[number]>;
};
```

Zod, Valibot, 和方舟型都暴露了 `.pick()` 或相当。 AJV的用户一般会用卡通签名来编译子验证器. 见Tsdoc上 `PickValidator` 以AJV为例.

以两个验证符配置面值 :

```ts
const TeamFacet = new Facet({
    name: 'TEAM',
    validator: teamValidator,
    pickValidator: teamPickValidator,
    PK: { keys: ['teamId'], prefix: '#TEAM' },
    SK: { keys: ['teamId'], prefix: '#TEAM' },
    connection: { dynamoDb: dynamoDbClient, tableName: dynamoDbTableName },
});
```

#### 打开 `get`

```ts
// Unprojected: the full record.
const team = await TeamFacet.get({ teamId });

// Projected: only the chosen fields plus the facet's PK/SK fields.
const slim = await TeamFacet.get({ teamId }, { select: ['teamName'] });
// slim: { teamId: string; teamName: string } | null
```

结果中总是包括PK和SK字段,即使略去 `select`。它们包含着身份:您需要它们将结果回转到 `get`, (中文(简体) ), `delete`,或 `put`。批次得到同样的方式:

```ts
const slim = await TeamFacet.get(
    teamIds.map((teamId) => ({ teamId })),
    { select: ['teamName'] },
);
```

#### 打开 `query`

每个 `PartitionQuery` 操作员接受 `select` 并同样缩小返回类型.

```ts
await TeamFacet.query({ teamId }).list({ select: ['teamName'] });

await TeamFacet.query({ teamId }).first({ select: ['teamName'] });

await TeamFacet.GSIByStatus.query({ teamId, status: 'active' }).between(
    { createdAt: '2024-01-01' },
    { createdAt: '2024-02-01' },
    { select: ['teamName'] },
);
```

在索引查询上,Faceteer自动包含基础表的PK/SK字段和索引自己的分区键和排序键字段. 根据图书馆的假设,GSI的创建方式是: `ProjectionType: ALL`,所有四套总是出现在索引上。 保证让你直接反馈到预测的结果 `get`, (中文(简体) ), `delete`,或 `put` 无需二次往返以获取身份字段.

#### 类型级门

在类型级别上无法投射未建构的方块 `pickValidator`。通过 `select` 上方是编译错误,而不是运行时间错误.

```ts
const PlainFacet = new Facet({/* no pickValidator */});

// Type error: this overload requires pickValidator on the facet.
await PlainFacet.get({ teamId }, { select: ['teamName'] });

// Also a type error:
await PlainFacet.query({ teamId }).list({ select: ['teamName'] });
```

如果想要预测读取,请添加 `pickValidator` 到面额选项。 如果您要选择退出对预测读取的验证( 信任- 数据库路径, 或基准) , 请通过一个身份选择符 :

```ts
const identityPickValidator: PickValidator<Team> = (keys) => (input) =>
    input as Pick<Team, (typeof keys)[number]>;
```

#### 费用

投影可以减少DynamoDB通过电线发送的数据,从而收缩了有效载荷大小和JSON-parse成本. 它不减少读取容量: DynamomDB根据它读取的项目大小从存储,而不是它预测到响应. 投影是一种带宽和延迟优化,而不是区域协调单位.

### 一致读取

DynamoDB的读取最终默认是一致的:读取一个紧跟着一个写作的读取可以还原项目更古老的状态. 当你需要读后写保证时,通过 `consistentRead: true` 选择一个非常一致的读取,它反映了每一个在读取之前成功的书写。 从全球来看,这一保障只存在于一个区域:一个非常一致的读取可以返回一个最近一次在另一个区域更新的项目的陈旧数据.

该选项可在单次获取、批次获取和基准表格查询中找到:

```ts
// Single get
const team = await TeamFacet.get({ teamId }, { consistentRead: true });

// Batch get
const teams = await TeamFacet.get([{ teamId: 'a' }, { teamId: 'b' }], {
    consistentRead: true,
});

// Base-table query
const { records } = await TeamFacet.query({ teamId }).list({
    consistentRead: true,
});
```

DynamoDB不支持在全局二级索引上读取一致, 因此索引查询门在类型级别关闭选项:

```ts
// Compile error: consistentRead is not available on index queries.
await PostFacet.GSIPostByTitle.query({ pageId }).list({ consistentRead: true });
```

`transactGet` 不需要选项: DynamoDB 交易是可序列的, 因此一个交易读取至少已经和一致读取一样强了 。 这也是最昂贵的读取方式: DynamoDB的帐单 以强烈一致的读取率的两倍读取,所以使用 `transactGet` 原子多项目快照,而不是替代 `consistentRead`。 。 。 .

高度一致的读取成本是最终一致读取的读取能力的两倍,并且具有更高的延迟度。 把它们保留在读取自己写作的路径上,让其他一切使用默认.

### 复合排序键

Faceteer 中的排序键是用前缀和您列出的字段构建的复合字符串 `SK.keys`。一个配置为 `SK: { keys: ['status', 'timestamp'], prefix: '#ESTIME' }` 写入排序键如 `#ESTIME_queued_2024-01-01T00:00:00.000Z`。 。 。 .

DynamoDB存储和比较这些排序的键作为平庸的字符串. `equals`, (中文(简体) ), `beginsWith`,以及 `between` 全部运行在复合字符串上,而不是您通过的各个字段上。 这通常是你想要的:

```ts
// Every record in the partition whose composite SK starts with '#ESTIME_queued_'.
await EmailFacet.byStatus.query({ userId }).beginsWith({ status: 'queued' });
```

`greaterThan`, (中文(简体) ), `greaterThanOrEqual`, (中文(简体) ), `lessThan`,以及 `lessThanOrEqual` 也可以比较整个复合字符串。 它们没有涉及主导领域的单一价值。 这些旅行的人:

```ts
// Anti-pattern. Bleeds across status values.
await EmailFacet.byStatus
    .query({ userId })
    .greaterThan({ status: 'queued', timestamp: '2024-01-01' });
```

DynamoDB每张唱片都问在哪里 `#ESTIME_queued_2024-01-01 < SK`中,包括记录 `status = 'queued'` 以及每张按字母顺序排列的记录 `queued`,例如 `sent` 或者说 `spam`操作员不可能知道你的意思是"排队".

正确射入后方的两种方法:

1.  使用 `between` 双方的界限被固定在相同的主值上。 两个字符串共享 `#ESTIME_queued_` 前缀,因此范围不能交叉到另一个状态.
    
    ```ts
    await EmailFacet.byStatus
     .query({ userId })
     .between(
         { status: 'queued', timestamp: '2024-01-01' },
         { status: 'queued', timestamp: '2024-02-28' },
     );
    ```
    
    对于打开的上行,使用一个按每个真实值排序的监视器,例如 `'\uffff'`。 。 。 .
    
2.  设计索引,使范围界定字段位于分区键中. 这就是任务的例子 `GSIUserStatusCreated` 索引显示 :
    
    ```ts
    // PK carries the scoping field; SK is free for a clean range.
    PK: { keys: ['assignedUserId', 'status'], prefix: '#USER_STATUS' },
    SK: { keys: ['dateCreated'],              prefix: '#TASK_CREATED' },
    ```
    
    有了这种形状,每个查询都隐含在一种状态中 `greaterThan({ dateCreated })` 意思是它看起来像它的意思.
    

**决定规则.** 如果字段每次只被询问一个值,请将其放入分区键中. 如果您想要一个通过字段值和二级属性排序的查询,请将其作为复合排序键和范围的主要条目 `beginsWith` 或同等范围 `between`。如果字段仅用于订购,则将其从密钥中留出,并选择不同的类型字段.

### 查询模式参考

直接寻找共同访问模式。 每个都假定面部有一个适合这种模式的索引.

**"每个记录在一个分区,排序顺序"**

```ts
await Facet.query({ partitionFields }).list();
```

**"每张SK领先的唱片都等于价值"**

```ts
await Facet.query({ partitionFields }).beginsWith({ leadingField: value });
```

**"距离在SK后方的球场, 范围为领先的SK球场的一个值"**

```ts
await Facet.query({ partitionFields }).between(
    { leadingField: value, trailingField: start },
    { leadingField: value, trailingField: end },
);
```

**"每个有特定字段值的记录,由另一个字段命令"**

使用带有 PK 范围界定字段和 SK 订购字段的索引.

```ts
// Index shape
PK: { keys: [...partitionFields, 'scopingField'], prefix: '#SCOPE' },
SK: { keys: ['orderingField'],                    prefix: '#ORDER' },

// Call
await Facet.byScopedOrder
  .query({ ...partitionFields, scopingField: value })
  .list();
```

**"最新记录"**

```ts
await Facet.query({ partitionFields }).first({ scanForward: false });
```

**"最新记录"**

```ts
await Facet.query({ partitionFields }).list({
    limit: N,
    scanForward: false,
});
```

**"被非关键属性过滤的分区记录"**

```ts
await Facet.query({ partitionFields }).list({
    filter: ['someField', '=', value],
});
```

`filter` 在密钥条件后运行服务器侧。 它会降低你所看到的,而不是你所付的读取能力.

**"这个分区里有没有记录?"**

```ts
const exists = (await Facet.query({ partitionFields }).first()) !== null;
```

**"把每个记录都分解出来"**

```ts
let cursor: string | undefined;
do {
    const page = await Facet.query({ partitionFields }).list({
        limit: 100,
        cursor,
    });
    for (const record of page.records) {
        // handle record
    }
    cursor = page.cursor;
} while (cursor);
```

**"每个记录,但只有子集属性"**

要求数 `pickValidator` 在表面上.

```ts
await Facet.query({ partitionFields }).list({ select: ['name', 'status'] });
```

PK/SK字段自动包含在结果中,在索引查询中,该索引的PK/SK字段也包含在内.

### 倾斜

DynamoDB 最多返回一次 **1 个 MB** 每页评价数据。 当获得更多数据时,服务返回 a `LastEvaluatedKey`; Faceteer 表面为不透明 `cursor` 字符串( CBOR, 然后是 Base64) 。 将光标当作一个符号 —— 不要试图分析它, 并且不要持续到当前会话之后 : 编码被绑定在这个库的版本和 DynamoDB SDK的 `AttributeValue` 形状.

有两件事让新用户大吃一惊:

1.  `limit` 封顶有多少项 DynamoDB **评价**没有多少匹配。 过滤器运行 **之后** 键性条件,所以带有过滤器的查询可以返回空 `records` 数组 _财务报告和审定财务报表_ (单位:千美元) `cursor` -那只是"继续"的意思.
2.  你玩够了 `cursor` 回来 `undefined`,而不是当 `records` 为空.

典型的相接相接相接相接相接相 :

```ts
let cursor: string | undefined;
do {
    const page = await TaskFacet.query({ teamId }).list({ limit: 50, cursor });
    for (const task of page.records) {
        // ...handle task
    }
    cursor = page.cursor;
} while (cursor);
```

### TTL 电话

设定 `ttl` 在字段名称的正上方,该字段包含一个无ix时间戳、一个数字字符串或一个 `Date`. Faceteer 将这个值写入 DynamoDB 的 TTL 属性中,这样服务就可以同步清除记录. AWS的扫瞄器一般会在48小时内删除过期的项目——如果新鲜度重要的话,过滤出已经过期的记录客户端.

```ts
const SessionFacet = new Facet({
    name: 'SESSION',
    PK: { keys: ['userId'], prefix: '#USER' },
    SK: { keys: ['sessionId'], prefix: '#SESSION' },
    connection: { dynamoDb: dynamoDbClient, tableName: dynamoDbTableName },
    validator: sessionValidator,
    ttl: 'expiresAt',
});
```

### 写硬化

如果一个分区键占据了狮子的写流量份额( 想想单曲) `teamId` 数以百万计的任务,DynamoDB 将停止物理分割。 添加一个 `shard` 配置到要分布在固定数桶的密钥 :

```ts
.addIndex({
  index: Index.GSI1,
  PK: {
    keys: ["postStatus"],
    shard: { count: 4, keys: ["postId"] },
    prefix: "#STATUS",
  },
  SK: { keys: ["sendAt"], prefix: "#STATUS" },
  alias: "GSIStatusSendAt",
});
```

在写作时, Faceteer CRC-32 将显示 `shard.keys` 字段,并预置六角硬ID到分区键上。 中间宽度是 `(count - 1).toString(16).length` 字符,这样 `count: 4` 给 `"0".."3"` 财务报告和审定财务报表 `count: 256` 给 `"00".."ff"`。 。 。 .

读取时,将硬数字直接传递给 `query(partition, shard)` (或经 `options.shard` 操作员。 查询每个硬和合并客户端以获得完整的分区.

### 交易

使用交易来应用一组解剖的写法:要么每个写都承诺,要么没有。 每个侧面都显示操作构建者 `.transaction`独立 `transactWrite` 函数在一个中执行它们 `TransactWriteItems` 电话.

支持3个写操作:

-   `put` 写完整记录,比如 `Facet.put`。 。 。 .
-   `delete` 删除记录的项目.
-   `check` 声称对某一物品有条件,但没有写信.

```ts
import { transactWrite } from '@faceteer/facet';

const result = await transactWrite([
    // Create the order.
    OrderFacet.transaction.put(order),
    // Only if the account is still active.
    AccountFacet.transaction.check(
        { accountId: order.accountId },
        { condition: ['status', '=', 'active'] },
    ),
]);

if (!result.wasSuccessful) {
    if (result.failures) {
        // One entry per operation, aligned with the array above: null for
        // operations that did not cause the cancellation, and a failure
        // with DynamoDB's reason code for each one that did.
        const [orderFailure, accountFailure] = result.failures;
        if (accountFailure?.conflictingItem) {
            // The account as it currently exists in the table, typed as Account.
            console.error(accountFailure.conflictingItem.status);
        }
    }
    throw result.error;
}
```

`transactWrite` 从来没有扔过。 DynamoDB 取消交易时,结果包含错误和 a `failures` 阵列。 其他失败, 如客户端守护器或网络错误, 只设置 `error`总是打开大门 `wasSuccessful` 而不是继续 `failures`。对于数组字面,每个位置都保持其外观类型,因此失败可以像上面的例子那样在结构上去结构化.

当现有项目的条件失败时, DynamoDB 返回该项目, Faceteer 将其解析为 `conflictingItem` 与面部的验证符。 这是表中目前存在的项目,这是乐观货币重审所需要的。 两个告诫:由于项目不存在而未能实现的条件,例如: `exists` 守护,不返回任何项目;如果返回的项目验证失败, `conflictingItem` 未设定时 `conflictingItemRaw` 仍然持有原始属性图.

如单项 `put` 财务报告和审定财务报表 `delete`,每个操作接受一个 `condition` 使用与 [条件设置](https://npmcn.com/package/%40faceteer/facet/#readme-conditional-puts)用于 `check` 需要具备条件.

交易最多接受100个业务。 操作可以来自不同的方面,包括不同表格上的方面,只要每个面都使用相同的DynamotB客户端. 两个针对同项的操作在网络呼叫前被拒绝.

要读取一个原子快照中的几个项目, 请使用 `transactGet`它执行同样的100操作上限和相同的客户要求 `transactWrite`。将每个操作作为单独的参数通过,这样,每个位置在 `items` 保持其正方形的记录类型 :

```ts
import { transactGet } from '@faceteer/facet';

const { items } = await transactGet(
    AccountFacet.transaction.get({ accountId }),
    OrderFacet.transaction.get({ accountId, orderId }),
);
// items is typed [Account | null, Order | null]
```

`transaction.get` 总是返回完整记录; 它不支持 `select` 预测.

## 问题

**为什么我要把每个关键领域都传到 `get`? 吗?** DynamoDB 完整识别一个项目 `(PK, SK)` 字段计算结果 `KeyConfiguration.keys`一小姐和Faceteer 无法建立它需要的复合密钥 来查看记录.

**我可以改变一个字段吗?** 没有 复合键是项目在DynamoDB中的身份;更改它意味着写出一新项目并删除旧项目.

**为什么我的查询返回的项目少于 `limit`? 吗?** 三个可能的原因: 你撞到1 MB 页面上限, a `filter` 删除事实后的结果,或者分区只是没有包含那么多项目。 检查 `cursor` ——如果它被定义的话,还有更多的读取.

**两个方面能否共享同样的全球指数?** 是的,这叫做指数超载,是单表设计的基石。 给每一面一个不同的 `prefix` 指数 `PK`编号 :`SK` 因此,您的询问可以瞄准您想要的项目.

**光标在部署或库升级时是否持久?** 没有 是DynamoDB的CBOR+base64编码 `LastEvaluatedKey`. 把它们当作会话- 本地符号; 永远不要把它们存放在某个地方, 您的应用程序可能会试图解码它们 .

**Faceteer是否在节奏上重试过?** 仅针对特定 `UnprocessedItems`编号 :`UnprocessedKeys` 批次内路径写出并读取(5个重写为写出,10个重写为读出,指数回放). 经常 `ProvisionedThroughputExceededException` 错误表现为被拒绝的承诺——如果你期望持续减速的话,就用回放来结束你的电话.

**对 `put` 在写作前验证我的记录?** 并非默认; 验证器运行在已读 。 过 `validateInput: true` 如果您想要验证也运行在写入上, 则切换到面板构造器 。 费用是每个电话的.

**对 `delete` 支助条件?** 只删除单个项目: `facet.delete(record, { condition: [...] })`。 。 。 . `condition` 与数组并列时被默默忽略.

## 最佳做法

-   **在设计密钥之前先了解你的访问模式.** Faceteer不是用来做临时查询的 每个GSI的写作费用, 所以选择一套最小的指数, 涵盖你实际拥有的模式.
-   **带有前缀的超载索引 .** 两个方面可以共享 `GSI1` 只要他们的 SK 前缀不同 - 这就是整个点 `prefix` 每个字段 `KeyConfiguration`。通过前缀进行区分,通过前缀进行查询.
-   **将关键条件置于首位 `filter`。 。 。 .** `equals`, (中文(简体) ), `beginsWith`,以及 `between` prune在服务器上读取. `filter` 也运行服务器侧, 但是 _之后_ 阅读是记帐的,它缩小了反应,而不是成本.
-   **默认最终一致读取 .** `consistentRead: true` 将读取容量成本翻倍并增加耐用性,所以保留给必须读取自己写作的路径,例如有条件的放出后立即读取. 它没有在GSI查询,和 `transactGet` 已经可以序列化 .
-   **到达 `select` 当有效载荷大小受伤时.** `ProjectionExpression` 切除电线有效载荷,但不减少读取能力成本。 DynamoDB不计全部项目大小 当您正在渲染列表、 水分缓存或通过慢链接返回时, 值得使用; 当你需要完整记录时, 也不值得使用.
-   **避免 `greaterThan` 财务报告和审定财务报表 `lessThan` 键.** 他们比较了完整的复合字符串,所以一个查询像 `greaterThan({ status: 'queued', timestamp: X })` 血流成河的记录 `queued`范围 `beginsWith`,或与 `between` 在两个边框上标出相同的主值。 见上文“综合排序键”部分.
-   **硬热分出.** 当一个PK值吸引了不成比例的流量时,添加一个 `shard: { count, keys }` 相会. 保留 `count` 小起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起起.
-   **TTL 用于平面数据 .** 会话、检察官办公室、缓存项和软删除标记都有好处。 记住 AWS 同步清除——过期的项目可以在查询结果中停留一段时间.
-   **指数很少.** Faceteer 只写原始值 / `Date` 字段进入复合键,因此留下索引字段 `undefined` 完全不记录在索引中 以此来制作廉价的"只开放命令"或"只活跃用户"索引.
-   **将PK和SK视为不可改变.** 任何有助于复合键的字段在写入后都无法改变. 提前计划,或计划迁移.
-   **遵守批量限制.** 每批写出25个物品,每批取出100个物品,1 MB响应上限适用于每个个别请求. 脸部切片给你, 但非常大的操作仍然 连续发生在许多回合.
-   **以重试逻辑来包接调用.** Faceteer的内部重试只覆盖"未处理的项目"路径. 其他油门是被否决的诺言.
-   **使用 `condition` 为乐观的货币.** 护卫创建 `['field', 'not_exists']`,通过版本检查来守护更新,针对种族守护删除. 提醒: `condition` 仅适用于单个项目的呼叫.
-   **读取验证.** Schemas 漂移; 在错误的数据到达您的商业逻辑之前, 验证器是您最后的防线 。 选择到 `validateInput: true` 仅当您特别想要写入时间验证时.

## 常见问题

### 如何安装 @faceteer/facet？

在项目目录运行 npm install @faceteer/facet，或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖：@aws-sdk/client-dynamodb（^3\.0\.0）。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖，Yarn 需要手动安装。

### @faceteer/facet 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @faceteer/facet 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @faceteer/facet 需要什么版本的 Node\.js？

package\.json 的 engines 字段要求 Node\.js \>=22\.12\.0。版本不符时 npm 默认只给出警告，开启 engine-strict 后会拒绝安装。

### @faceteer/facet 可以商用吗？

@faceteer/facet 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### @faceteer/facet 的下载量有多少？

根据 npm 官方下载统计，@faceteer/facet 在 2026/10/14 – 2026/11/12 这 30 天内被下载 3,572 次，日均约 119 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @faceteer/facet 还在维护吗？

最新版本 6\.1\.0 发布于 2026/08/22（2 个月前）；至今共发布 19 个版本，其中过去 12 个月发布了 3 个；首次发布于 2021/07/10。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

