@aws-blocks/bb-data

中文文档
0.5.0发布于 2026/11/02Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 6.4万

Full PostgreSQL database — provisions Aurora Serverless v2 by default, or connects to an existing PostgreSQL database (Supabase, Neon, etc.) via fromExisting(). Full relational modeling with foreign keys, transactions, Row Level Security, and a type-saf

原始使用文档
文档目录15 节
  1. Quick Start
  2. Migrations
  3. Kysely Query Builder
  4. Row Level Security (RLS)
  5. CRUD Handlers
  6. Connecting to an Existing Database
  7. TLS certificate verification
  8. Migrating from Supabase
  9. Error Handling
  10. What It Provisions (AWS)
  11. Local Development
  12. Local vs. AWS: column types
  13. Configuration
  14. Package Export Conditions
  15. Performance

@aws-blocks/bb-data

Full PostgreSQL database — provisions Aurora Serverless v2 by default, or connects to an existing PostgreSQL database (Supabase, Neon, etc.) via fromExisting(). Full relational modeling with foreign keys, transactions, Row Level Security, and a type-safe Kysely query builder.

When to use: Complex multi-table JOINs, ACID transactions, foreign key constraints, aggregations, Row Level Security, or connecting to an existing PostgreSQL database. Use when you need the full power of PostgreSQL.

When NOT to use: For simple key-value lookups, use KVStore. For NoSQL with secondary indexes, use DistributedTable. For serverless SQL without FK/RLS/triggers (multi-region, instant provisioning), use DistributedDatabase.

Design & mock parity details: DESIGN.md

Quick Start

import { Database, sql } from '@aws-blocks/bb-data';

const db = new Database(scope, 'main', {
  migrationsPath: './aws-blocks/migrations',
});

// Parameterized queries via sql tagged template (injection-safe)
const users = await db.query<{ id: string; name: string }>(
  sql`SELECT * FROM users WHERE active = ${true}`
);

const user = await db.queryOne<{ id: string; name: string }>(
  sql`SELECT * FROM users WHERE id = ${userId}`
);

const { rowCount } = await db.execute(
  sql`INSERT INTO users (id, name, email) VALUES (${id}, ${name}, ${email})`
);

// Transactions
await db.transaction(async (tx) => {
  await tx.execute(sql`UPDATE accounts SET balance = balance - ${100} WHERE id = ${fromId}`);
  await tx.execute(sql`UPDATE accounts SET balance = balance + ${100} WHERE id = ${toId}`);
});

Migrations

Create numbered .sql files in a migrations directory:

aws-blocks/migrations/
  001_create_users.sql
  002_create_posts.sql
  003_seed_admin.sql
-- 001_create_users.sql
CREATE TABLE users (
  id TEXT PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Migrations run automatically:

  • Local dev: On first query (PGlite, persists in .bb-data/)
  • AWS deploy: Via a CustomResource Lambda during cdk deploy

Applied migrations are tracked in a _migrations table. Each file runs once.

Set migrationsPath to a path relative to your project root (as in the Quick Start — './aws-blocks/migrations'); it's resolved at synth from the directory you run cdk / npm run deploy in. That's the simplest reliable pattern.

You don't need fileURLToPath(import.meta.url) for this. Your backend module runs as ESM locally but is bundled to CommonJS in Lambda, where import.meta is empty. AWS Blocks shims import.meta.url / import.meta.dirname in the bundle so it won't crash at load — but at runtime those resolve to the bundled output location, not your source tree. So don't use import.meta.url to read a file relative to your source at request time; inline the data or ship it as a Lambda asset instead.

Kysely Query Builder

For type-safe queries without raw SQL:

import { createKyselyAdapter } from '@aws-blocks/bb-data';

interface Schema {
  users: { id: string; email: string; name: string };
  posts: { id: string; user_id: string; title: string };
}

const kysely = createKyselyAdapter<Schema>(db);

// Type-safe SELECT
const users = await kysely
  .selectFrom('users')
  .where('email', '=', '[email protected]')
  .selectAll()
  .execute();

// JOINs
const posts = await kysely
  .selectFrom('posts')
  .innerJoin('users', 'users.id', 'posts.user_id')
  .select(['posts.title', 'users.name'])
  .execute();

// Transactions
await kysely.transaction().execute(async (trx) => {
  await trx.insertInto('users').values({ id: '1', email: '[email protected]', name: 'A' }).execute();
  await trx.insertInto('posts').values({ id: '1', user_id: '1', title: 'Hello' }).execute();
});

See Kysely documentation for the full query builder API.

Row Level Security (RLS)

Scope queries to a user context with Supabase-compatible session variables:

const scoped = db.withRLS({ userId: 'user-123', role: 'authenticated' });

// All queries on `scoped` run inside a transaction with SET LOCAL ROLE
// and request.jwt.claims set — PostgreSQL RLS policies are enforced.
const myPosts = await scoped.query<Post>(sql`SELECT * FROM posts`);

request.jwt.claims always carries sub = userId and role = the role you pass. Extra claims are merged in, but they can't override those two: a sub or role key in claims is ignored. Policies can therefore trust both, for example USING (owner_id = current_setting('request.jwt.claims', true)::jsonb ->> 'sub'). When you forward claims from an identity provider, put provider-specific values under their own keys (for example claims: { idp_sub: token.sub }).

Prerequisite (local and AWS): withRLS issues SET LOCAL ROLE <role> (default authenticated). The role must exist, and the database user must be a member of it. On PostgreSQL 16 and later, which Aurora runs, the user that creates a role gets only ADMIN on it, not SET, so grant the roles explicitly. Add to your migrations:

CREATE ROLE authenticated;
CREATE ROLE anon;
GRANT authenticated TO CURRENT_USER;
GRANT anon TO CURRENT_USER;
-- grant table privileges to these roles as needed, then define RLS policies

If the role switch is refused, every withRLS() call fails with a 500 QueryFailed, and the server log names the missing GRANT.

CRUD Handlers

Generate typed CRUD methods from a schema definition:

const crud = db.crud({
  tables: ['users', 'posts'],
  // `auth` takes no arguments — close over your request context to resolve the user.
  auth: async () => {
    const user = await auth.requireAuth(context);
    return { userId: user.userId };
  },
});

// Auto-generated flat method names per table:
//   crud.listUsers(), crud.getUser(id), crud.createUser(data),
//   crud.updateUser(id, data), crud.deleteUser(id)
//   crud.listPosts(), crud.getPost(id), ...

Connecting to an Existing Database

import { Database, fromExisting } from '@aws-blocks/bb-data';

// Supabase, Neon, or any PostgreSQL-compatible database
const db = new Database(scope, 'external', {
  connection: fromExisting({ connectionString: process.env.DATABASE_URL! }),
});

TLS certificate verification

The server's TLS certificate is verified by default. Managed providers (Supabase, Neon, RDS) present a certificate signed by a provider-specific CA that is not in Node's built-in trust store, so verification requires pinning that CA. ssl.ca takes the certificate contents (a PEM string), not a path — for Supabase, download prod-ca-2021.crt from your project's Database Settings → SSL Configuration:

import { readFileSync } from 'node:fs';

const db = new Database(scope, 'external', {
  connection: fromExisting({
    connectionString: process.env.DATABASE_URL!,
    ssl: { ca: readFileSync('./supabase-ca.crt', 'utf8') },
  }),
});

bb-data pull wires this for you: it prompts for your CA certificate and commits it to aws-blocks/database.ca.ts (a public, non-secret cert that is bundled into your deployed function), so the connection is verified by default — including in the deployed Lambda, with no runtime configuration. DATABASE_CA_CERT (inline PEM or a file path) overrides the committed cert. If neither is available, the generated wiring falls back to ssl: { rejectUnauthorized: false } (encrypted but unauthenticated) in local dev only; the deployed function fails closed (refuses to connect) rather than running unverified. Provide the CA for production.

Migrating from Supabase

Already have a Supabase app? bb-data pull connects to your existing Supabase database and generates a complete, type-safe backend — keeping your tables, data, and RLS policies exactly as they are.

npx bb-data pull

What it does:

  • Introspects your public-schema tables (read-only — your database is not modified)
  • Generates typed definitions, CRUD operations, and a personalized migration guide
  • Stores your connection string locally as SUPABASE_DB_URL in .env.local (encrypted in SSM on deploy). Only that entry is added or updated — other settings, comments, and the file's permissions are kept; the SUPABASE_DB_URL line itself is rewritten whole, so a comment at the end of it is not kept. If .env.local can't be updated safely (for example, the change would alter how another entry is read), db pull stops before generating any files and asks you to set the entry by hand. Choosing "production" does the same for .env.production.

What it does NOT migrate: Supabase Auth, Storage, Realtime, or Edge Functions. If you use a third-party OIDC provider (Auth0, Clerk, Google, Cognito), you can wire it into Blocks — see the generated MIGRATION_GUIDE.md#auth.

After pulling, run npm run dev to start developing locally against your real database.

Once pulled, manage schema changes with version-controlled SQL migrations in ./migrations/ — applied automatically on npm run dev and npm run deploy. See the generated MIGRATION_GUIDE.md#evolving-your-schema.

Error Handling

import { DatabaseErrors } from '@aws-blocks/bb-data';
import { isBlocksError } from '@aws-blocks/core';

try {
  await db.execute(sql`INSERT INTO users (id, email) VALUES (${id}, ${email})`);
} catch (e: unknown) {
  if (isBlocksError(e, DatabaseErrors.UniqueConstraintViolation)) {
    // Duplicate key — email already exists. Serialized as HTTP 409 (Conflict),
    // not retriable (a blind retry of the same insert fails identically).
  }
  if (isBlocksError(e, DatabaseErrors.PermissionDenied)) {
    // Your statement was refused: a row-level-security policy rejected the row
    // (for example a withRLS() user writing someone else's row), or the role has
    // no grant on the table. Serialized as HTTP 403.
  }
  if (isBlocksError(e, DatabaseErrors.CheckViolation) || isBlocksError(e, DatabaseErrors.NotNullViolation)) {
    // A value fails a CHECK constraint, or a required column is missing.
    // Serialized as HTTP 400.
  }
  if (isBlocksError(e, DatabaseErrors.ForeignKeyViolation)) {
    // The referenced row doesn't exist, or other rows still reference the one
    // being deleted. Serialized as HTTP 409 (Conflict).
  }
  if (isBlocksError(e, DatabaseErrors.QueryFailed)) {
    // General query failure (syntax error, missing table, etc.)
  }
  if (isBlocksError(e, DatabaseErrors.TransactionFailed)) {
    // Transaction could not commit
  }
  if (isBlocksError(e, DatabaseErrors.SerializationFailure)) {
    // Serializable-isolation conflict with a concurrent transaction — serialized
    // as HTTP 409 (Conflict), retriable — safe to retry
  }
  if (isBlocksError(e, DatabaseErrors.ConnectionFailed)) {
    // Cannot reach the database — includes a `minCapacity: 0` cluster resuming
    // from auto-pause, which succeeds on retry a few seconds later
  }
}

The client-visible message of each error is a fixed string; the driver's own text, which names tables, columns and constraints, stays on the server as the error's cause. Only SerializationFailure and ConnectionFailed are worth retrying: the request errors (PermissionDenied, CheckViolation, NotNullViolation, ForeignKeyViolation, UniqueConstraintViolation) fail the same way every time.

Each of these errors aborts the enclosing Postgres transaction. Inside a db.transaction() callback, a caught error leaves the transaction unusable: the next statement fails as QueryFailed. Catch them outside transaction(), or wrap the statement in a savepoint (SAVEPOINT s … ROLLBACK TO SAVEPOINT s) if the transaction should continue.

What It Provisions (AWS)

  • Aurora Serverless v2 — PostgreSQL-compatible, scales 0.5-128 ACUs. Storage encryption at rest is opt-in via a context flag so new projects are secure by default without replacing an existing cluster. New create-blocks-app projects set "@aws-blocks/bb-data:encryptStorageByDefault": true in cdk.json, which turns on encryption with the account's AWS-managed aws/rds key. Passing a storageEncryptionKeyArn (the ARN of a customer-managed KMS key) also turns encryption on regardless of the flag and uses that key instead (it also encrypts the generated credentials secret). When neither the flag nor a key is set, the cluster is synthesized without the StorageEncrypted property (an existing, unencrypted cluster is left untouched) and a synth warning explains how to opt in. The key is imported (Blocks can't modify its policy), so its policy must already grant the deploying principal kms:CreateGrant + kms:DescribeKey (RDS uses a grant to encrypt the storage volume) and grant kms:Decrypt to the principal that reads the credentials secret over the Data API. The key must be in the same account and region as the cluster.
  • Automated backups — on by default, retained 15 days, which is also the point-in-time-recovery window. Controlled by the single pointInTimeRecovery option (mirrors every other Blocks block): true enables the 15-day window, { retentionDays: n } pins a 1–35-day window, and false clamps to the 1-day minimum (Aurora cannot turn automated backups off). When omitted it follows the stack-wide defaults.pointInTimeRecovery (on under production, off under sandbox — which lands on the 1-day minimum).
  • CloudWatch log export — the PostgreSQL engine log is exported to CloudWatch Logs. Log-group retention follows the stack-wide defaults.logRetention when set; otherwise it uses the account default.
  • VPC — Private subnets (isolated, no NAT)
  • Secrets Manager — Auto-generated credentials. Automatic rotation is not yet wired up (a rotation Lambda in the cluster VPC is a planned follow-up); the secret is encrypted with your storageEncryptionKeyArn when one is supplied.
  • Migration Lambda — Runs .sql files on deploy via CustomResource
  • IAM — rds-data:* and secretsmanager:GetSecretValue granted to the app Lambda

Access model: the database is reached exclusively over the RDS Data API (HTTPS + Secrets Manager credentials), never a raw Postgres socket. Database-level IAM authentication (iamAuthentication) is therefore intentionally left off — it does not apply to the Data API access path.

Enabling encryption on an existing unencrypted cluster requires a replacement — and a replacement is destructive. RDS cannot encrypt an already-provisioned unencrypted cluster in place, so turning storage encryption on (or changing the KMS key) makes CloudFormation create a new, empty encrypted cluster and repoint the stack at it. There is no in-place path. What happens to the old cluster and its data depends on the removal policy:

  • Production (RETAIN): the old cluster is left behind, orphaned and no longer referenced by the stack. The app comes back pointed at the new empty cluster. The migration CustomResource does not re-run on a cluster swap — its only CloudFormation properties are the service token and the migrations hash, and neither changes when the cluster is replaced, so CloudFormation never invokes it. The replacement cluster therefore comes up with no schema; migrations run only when a migration file changes (which changes the migrations hash). Your data still exists on the orphaned cluster but is not restored.
  • Sandbox (DESTROY): the old cluster and all of its data are deleted.
  • Snapshot (removalPolicy: 'snapshot'): the old cluster is snapshotted as it is replaced. Under production's deletionProtection: true, the follow-up delete of the old cluster may fail and leave it in place; this cleanup path is unverified on a real deploy.

The replacement is symmetric. Turning encryption back off — removing the flag (or storageEncryptionKeyArn) from a project that already deployed encrypted, whether by deleting the cdk.json line or by a merge that drops it — also changes the StorageEncrypted property, so it likewise replaces the cluster (back to unencrypted) with the same new-empty-cluster / no-schema outcome described above. Review the diff and snapshot before removing the flag too, not only before adding it.

The only safe route is to carry the data across yourself: snapshot the existing cluster, restore that snapshot into a new cluster with encryption enabled, then cut over to it. Review the CloudFormation diff and take a snapshot before deploying this change to any cluster whose data you need. Adding or changing the KMS key also replaces the cluster's generated credentials secret (a new logical id, hence a new generated password), so record the existing credentials — or reset the master password on the old cluster — if you still need access to the orphaned cluster.

Local Development

  • Engine: PGlite (WASM PostgreSQL) — full Postgres compatibility
  • Storage: .bb-data/{fullId}/ — persists across restarts, wipe with rm -rf .bb-data
  • Migrations: Run automatically on first query

Local vs. AWS: column types

Array columns and json / jsonb columns return the same JavaScript values on both engines. Array columns (text[], int[], ...) come back as JS arrays and json / jsonb columns come back as parsed objects, whether you run locally on PGlite or on AWS over the RDS Data API. Element representation still follows the scalar notes below — e.g. numeric[] elements come back as strings over the Data API.

Some scalar numeric types can still differ in representation between the two engines: numeric / decimal comes back as a string over the Data API, and bigint / int8 comes back as a JS number over the Data API while the pg / PGlite driver returns it as a string by default. Cast or parse these explicitly if you need identical values across both engines.

One Postgres type the RDS Data API cannot return is tsvector: selecting a tsvector column directly (for example select * on a table with a generated search tsvector column) fails on AWS. Exclude it from the select list — a tsvector is normally only used in a WHERE / ORDER BY full-text predicate, not read back as a value.

Configuration

interface DatabaseOptions {
  /** Path to directory containing numbered .sql migration files. */
  migrationsPath?: string;
  /** Connect to an existing database instead of provisioning one. */
  connection?: ExternalDatabaseRef;
  /** Schema metadata for crud() support. */
  schema?: TableSchema;
  /** Aurora PostgreSQL engine version, e.g. '16.13'. Override the Aurora engine version. @default '16.13' */
  postgresVersion?: string;
  /**
   * ARN of a customer-managed KMS key for the cluster's storage-at-rest encryption
   * (also encrypts the auto-generated credentials secret). Supplying it turns
   * storage encryption on regardless of the `@aws-blocks/bb-data:encryptStorageByDefault`
   * context flag. When omitted, encryption follows that flag — on with the
   * account's AWS-managed `aws/rds` key when set, otherwise left unset. The key is
   * imported, so its policy must already grant the deploying principal
   * `kms:CreateGrant` + `kms:DescribeKey` and grant `kms:Decrypt` to the principal
   * reading the credentials secret; it must be in the same account + region.
   */
  storageEncryptionKeyArn?: string;
  /**
   * Automated-backup retention, which is also the point-in-time-recovery (PITR)
   * window. `true` enables the 15-day window; `{ retentionDays: n }` pins a
   * 1–35-day window; `false` clamps to the 1-day minimum (Aurora cannot disable
   * automated backups). When omitted, follows `defaults.pointInTimeRecovery`.
   */
  pointInTimeRecovery?: boolean | { retentionDays: number };
}

Package Export Conditions

{
  "exports": {
    ".": {
      "cdk": "./dist/index.cdk.js",
      "aws-runtime": "./dist/index.aws.js",
      "default": "./dist/index.mock.js"
    }
  }
}

Performance

  • Query latency: 10-50ms (warm), ~500ms cold start from 0 ACUs
  • Throughput: Requests go over the RDS Data API (HTTPS) rather than pooled database connections, so there is no connection-pool ceiling or RDS Proxy; concurrency is bounded by the Data API request limits and the cluster's ACU scaling.
  • Storage: Up to 128 TiB, auto-scales in 10 GiB increments
  • Cost: ~$0.12/ACU-hour + ~$0.10/GB-month storage
  • Durability: 6 copies across 3 AZs, 99.99% availability

版本与发布记录

@aws-blocks/bb-data 共发布 15 个版本,首次发布于 2026/06/16;过去 12 个月发布了 15 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.5.0latest2026/11/025 天前
0.4.02026/10/1622 天前
0.0.0-canary.esql-canary.c93c81b.1791235863canary-esql-canary2026/10/061 个月前
0.3.12026/10/051 个月前
0.0.0-canary.esql-canary.f7f39ac.17909891272026/10/031 个月前
0.0.0-canary.ci-publish-canary-npm.f53dfdc.1790988617canary-ci-publish-canary-npm2026/10/031 个月前

关于 @aws-blocks/bb-data 的常见问题

@aws-blocks/bb-data 是什么?

Full PostgreSQL database — provisions Aurora Serverless v2 by default, or connects to an existing PostgreSQL database (Supabase, Neon, etc.) via fromExisting(). Full relational modeling with foreign keys, transactions, Row Level Security, and a type-saf

如何安装 @aws-blocks/bb-data?

在项目目录运行 npm install @aws-blocks/bb-data,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:kysely(>=0.28.12 <0.30.0)、constructs(^10.6.0)、aws-cdk-lib(^2.257.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。它还提供命令行工具 bb-data,可以用 npx bb-data 直接运行。

@aws-blocks/bb-data 支持 TypeScript 吗?

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

@aws-blocks/bb-data 支持 ESM 还是 CommonJS?

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

@aws-blocks/bb-data 可以商用吗?

@aws-blocks/bb-data 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@aws-blocks/bb-data 的下载量有多少?

根据 npm 官方下载统计,@aws-blocks/bb-data 在 2026/10/07 – 2026/11/05 这 30 天内被下载 64,487 次,日均约 2,150 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@aws-blocks/bb-data 还在维护吗?

最新版本 0.5.0 发布于 2026/11/02(5 天前);至今共发布 15 个版本,其中过去 12 个月发布了 15 个;首次发布于 2026/06/16。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。