@faceteer/facet

中文文档
6.1.0发布于 2026/08/22MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 3572

Faceteer

中文使用文档
文档目录19 节
  1. 开始
  2. 安装
  3. 访问模式
  4. 验证
  5. 创建面板
  6. 记录
  7. 获取记录
  8. 查询
  9. 索引
  10. 预计为:
  11. 一致读取
  12. 复合排序键
  13. 查询模式参考
  14. 倾斜
  15. TTL 电话
  16. 写硬化
  17. 交易
  18. 问题
  19. 最佳做法

Faceteer

codecov Test npm version

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

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

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

亚马逊迪纳摩DB深潜

开始

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

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

安装

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

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

Faceteer需要22.12节节点或更新.

访问模式

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

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

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

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

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

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

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 到侧面构造器。 这增加了每个电话费用,但默认取消.

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 属性,使您在多个方面共享一个表格时区分项目类型.

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,
});

记录

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

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。 。 。 .

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

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 }。 。 。 .

// 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通过两个密钥的组合在表格中独有地识别记录.

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倍 与指数反射。 返回的数组无法保证保存输入顺序 .

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

查询

查询记录分两部分进行.

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

import { UserFacet } from './facets';

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

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

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 编号。 过滤器使用与 条件设置 (仅限于比较者, between,以及 begins_with),并在关键条件后运行在服务器上,因此它们缩小了响应而不是读取成本.

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

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

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

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() 函数.

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

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

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

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

索引

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

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

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

这就是我们的一面:

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 排序顺序匹配按排序生成的任务 。 这包含了我们的一个进入模式

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

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

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 以查询我们的任务的预定日期!

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

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

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 可读性.

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,
    };
}

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

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

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

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,...)必须在您的桌面上宣布您实际使用的任何索引的计划.

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

预计为:

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

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

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为例.

以两个验证符配置面值 :

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

// 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。批次得到同样的方式:

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

打开 query

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

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 上方是编译错误,而不是运行时间错误.

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 到面额选项。 如果您要选择退出对预测读取的验证( 信任- 数据库路径, 或基准) , 请通过一个身份选择符 :

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

费用

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

一致读取

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

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

// 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不支持在全局二级索引上读取一致, 因此索引查询门在类型级别关闭选项:

// 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 全部运行在复合字符串上,而不是您通过的各个字段上。 这通常是你想要的:

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

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

// 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_ 前缀,因此范围不能交叉到另一个状态.

    await EmailFacet.byStatus
     .query({ userId })
     .between(
         { status: 'queued', timestamp: '2024-01-01' },
         { status: 'queued', timestamp: '2024-02-28' },
     );
    

    对于打开的上行,使用一个按每个真实值排序的监视器,例如 '\uffff'。 。 。 .

  2. 设计索引,使范围界定字段位于分区键中. 这就是任务的例子 GSIUserStatusCreated 索引显示 :

    // 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。如果字段仅用于订购,则将其从密钥中留出,并选择不同的类型字段.

查询模式参考

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

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

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

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

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

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

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

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

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

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

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

"最新记录"

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

"最新记录"

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

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

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

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

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

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

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

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 在表面上.

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 为空.

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

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小时内删除过期的项目——如果新鲜度重要的话,过滤出已经过期的记录客户端.

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 配置到要分布在固定数桶的密钥 :

.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 声称对某一物品有条件,但没有写信.
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 使用与 条件设置用于 check 需要具备条件.

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

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

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 共发布 19 个版本,首次发布于 2021/07/10;过去 12 个月发布了 3 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
6.1.0latest2026/08/222 个月前
6.0.02026/04/206 个月前
6.0.0-alpha.0alpha2026/04/206 个月前
5.0.02025/08/051.3 年前
4.0.52022/04/124 年前
4.0.32022/02/034 年前

其他发布通道:beta → 1.0.0-beta.3,alpha → 6.0.0-alpha.0。

关于 @faceteer/facet 的常见问题

如何安装 @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 和提交记录判断。