UNPKG

rotorise

Version:

Supercharge your DynamoDB with Rotorise!

199 lines (140 loc) 5.25 kB
# Rotorise Type-safe DynamoDB composite key management for TypeScript. DynamoDB offers incredible flexibility, but managing advanced patterns and techniques can be a challenge. Rotorise simplifies complex operations by providing abstractions for key definitions, composite key constructors, partial composite keys, and advanced sort key usage in queries. It integrates seamlessly with [Brushless](https://github.com/josher8a/Brushless) for a frictionless and performant DynamoDB experience. ## Installation ```bash npm install rotorise ``` ## Quick Start Define your entity type and schema. Rotorise infers the exact key string types. ```ts import { tableEntry } from 'rotorise' type User = { orgId: string id: string role: 'admin' | 'user' | 'guest' email: string } const UserTable = tableEntry<User>()({ PK: ['orgId', 'id'], SK: ['role'], GSI1PK: ['role'], GSI1SK: 'email', }) ``` ## API ### `tableEntry<Entity>()(schema, separator?)` Entry point for defining a DynamoDB table schema. Returns an object with the methods below. The double-call `<Entity>()(schema)` is required because TypeScript does not support partial type parameter inference — `Entity` is explicit while `Schema` is inferred from arguments. The optional `separator` defaults to `'#'`. ### `.key(keyName, attributes, config?)` Builds a specific key value from the given attributes. ```ts UserTable.key('PK', { orgId: 'acme', id: '123' }) // => 'ORGID#acme#ID#123' UserTable.key('GSI1SK', { email: 'a@b.com' }) // => 'a@b.com' ``` **`config` options:** - **`depth`** — Limit composite key to the first N components. Useful for `begins_with` queries. - **`allowPartial`** — When `true`, stops building the key when an attribute is missing instead of throwing. Returns a union of all valid partial prefixes at the type level. - **`enforceBoundary`** — When `true`, appends a trailing separator if the key is partial. Ensures a `begins_with` query doesn't match unintended prefixes. ```ts UserTable.key('PK', { orgId: 'acme' }, { allowPartial: true }) // => 'ORGID#acme' // Type: 'ORGID#acme' | `ORGID#${string}#ID#${string}` UserTable.key('PK', { orgId: 'acme', id: '1' }, { depth: 1 }) // => 'ORGID#acme' UserTable.key('PK', { orgId: 'acme', id: '1' }, { depth: 1, enforceBoundary: true }) // => 'ORGID#acme#' ``` ### `.toEntry(item)` Converts a raw entity into a complete DynamoDB item with all keys computed. ```ts const item = UserTable.toEntry({ orgId: 'acme', id: '123', role: 'admin', email: 'a@b.com', }) // => { orgId: 'acme', id: '123', role: 'admin', email: 'a@b.com', // PK: 'ORGID#acme#ID#123', SK: 'ROLE#admin', // GSI1PK: 'ROLE#admin', GSI1SK: 'a@b.com' } ``` Rejects excess properties at the type level. ### `.fromEntry(entry)` Strips computed keys from a table entry, returning the raw entity. ```ts const user = UserTable.fromEntry(item) // => { orgId: 'acme', id: '123', role: 'admin', email: 'a@b.com' } ``` ### `.infer` Zero-runtime inference helper. Use with `typeof` to get the full table entry type. ```ts type UserEntry = typeof UserTable.infer ``` ### `.path()` Creates a proxy that builds DynamoDB expression paths as strings. ```ts UserTable.path().email.toString() // => 'email' UserTable.path().PK.toString() // => 'PK' ``` ## Advanced Features ### Transforms Override how a field maps to its key segment using a transform function. ```ts const Table = tableEntry<User>()({ PK: [ ['orgId', (id: string) => ({ tag: 'ORG', value: id })], ['id', (id: string) => ({ tag: 'USER', value: id })], ], SK: ['role'], }) Table.key('PK', { orgId: 'acme', id: '123' }) // => 'ORG#acme#USER#123' ``` A transform returns either: - A `joinable` value (the field name uppercased becomes the tag) - `{ value }` (no tag segment emitted) - `{ tag, value }` (custom tag) ### Discriminated Schemas When your table stores a union of entity types, use a discriminator to define per-variant key specs. ```ts type Item = | { kind: 'order'; orderId: string; userId: string } | { kind: 'refund'; refundId: string; orderId: string } const ItemTable = tableEntry<Item>()({ PK: { discriminator: 'kind', spec: { order: ['userId', 'orderId'], refund: ['orderId', 'refundId'], }, }, SK: ['kind'], }) ItemTable.key('PK', { kind: 'order', userId: 'u1', orderId: 'o1' }) // => 'USERID#u1#ORDERID#o1' ItemTable.key('PK', { kind: 'refund', orderId: 'o1', refundId: 'r1' }) // => 'ORDERID#o1#REFUNDID#r1' ``` Set a discriminated spec value to `null` to produce `undefined` (useful for GSIs that don't apply to all variants). ### Custom Separator ```ts const Table = tableEntry<User>()(schema, '-') // Keys use '-' instead of '#': 'ORGID-acme-ID-123' ``` ## Error Handling All runtime errors throw `RotoriseError` (exported), so you can distinguish library errors from your own. ```ts import { RotoriseError } from 'rotorise' try { Table.key('PK', { /* missing required attrs */ }) } catch (e) { if (e instanceof RotoriseError) { /* ... */ } } ``` ## Contributing Open an [issue](https://github.com/josher8a/Rotorise/issues) or a PR. We are open to any kind of contribution and feedback. ## License [Apache-2.0](LICENSE)