rotorise
Version:
Supercharge your DynamoDB with Rotorise!
199 lines (140 loc) • 5.25 kB
Markdown
# 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)