@verisure-italy/dynamo-kit
v1.9.1
Published
DynamoDB utility library
Downloads
177
Readme
@verisure-italy/dynamo-kit
Shared DynamoDB utility layer for the monorepo. It provides configuration resolution, client setup, expression builders, and a typed repository abstraction for common CRUD operations.
Installation
pnpm add @verisure-italy/dynamo-kitMain Exports
getDynamoClient()resolveConfig()physicalTableName()createRepo()- expression helpers such as
buildProjection(),buildUpdate(),buildKeyCondition(), andbuildFilterExpression() - shared runtime types such as
DynamoConfig,Repo,FilterCondition, andPage
What This Package Gives You
- one cached
DynamoDBDocumentClientper effective config - environment-aware configuration resolution for local and cloud setups
- table-prefix handling through
physicalTableName() - a small repository abstraction for the most common CRUD and query flows
- lower-level expression helpers when you need to drop down to raw AWS SDK commands
Typical Flow
- Resolve config from the environment or explicit overrides.
- Build or reuse a
DynamoDBDocumentClientwithgetDynamoClient(). - Create a typed repo with
createRepo<TEntity>(). - Use
put,get,update,remove,scan, orqueryIndex.
getDynamoClient() caches clients by config and uses these document-client options by default:
convertEmptyValues: trueremoveUndefinedValues: trueconvertClassInstanceToMap: truewrapNumbers: false
DynamoConfig
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| region | string | No | AWS region. Defaults to AWS_REGION or eu-west-1 |
| endpoint | string | No | Custom DynamoDB endpoint, typically local DynamoDB |
| tablePrefix | string | No | Prefix applied by physicalTableName() |
| credentials | AWS credentials or provider | No | Explicit credentials override |
Environment Resolution
resolveConfig() combines explicit overrides and environment variables:
| Source | Description |
| --- | --- |
| AWS_REGION | Default AWS region |
| DYNAMO_ENDPOINT | Custom DynamoDB endpoint |
| DYNAMO_TABLE_PREFIX | Table prefix |
| IS_OFFLINE / AWS_SAM_LOCAL | Enables http://localhost:8000 as the default local endpoint |
If you run locally and do not pass an explicit endpoint, resolveConfig() falls back to http://localhost:8000 when IS_OFFLINE or AWS_SAM_LOCAL is truthy.
Quick Start
import { createRepo, getDynamoClient } from '@verisure-italy/dynamo-kit'
type User = {
id: string
username: string
roles: string[]
}
const client = getDynamoClient({
endpoint: 'http://localhost:8000',
tablePrefix: 'dev',
})
const userRepo = createRepo<User>({
baseTableName: 'users',
id: { idField: 'id' },
client,
})
const created = await userRepo.put({
id: 'user-1',
username: 'admin',
roles: ['ROLE_AAA_ADMIN'],
})
const sameUser = await userRepo.get('user-1')createRepo() Options
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| baseTableName | string | Yes | Logical table name before prefix resolution |
| tableName | string | No | Fully resolved table name. Use this to bypass physicalTableName() |
| client | DynamoDBDocumentClient | No | Custom client instance |
| id.idField | keyof TEntity | No | Single-field primary key, such as id |
| id.keyOf | (entityOrId) => Record<string, any> | No | Custom key builder for composite keys |
Use id.idField for the common single-partition-key case. Use id.keyOf when your table key is composite and you want the repo to derive the full key object.
Repository API
createRepo<TEntity>() returns a Repo<TEntity> with the following operations:
| Method | Purpose | Notes |
| --- | --- | --- |
| put(item) | Insert or replace an item | Adds createdAt and updatedAt if missing |
| get(id, fields?) | Fetch one item by id | Supports field projection |
| update(id, patch) | Apply a partial update | Always refreshes updatedAt |
| remove(id) | Delete one item | No return value |
| scan(params) | Scan a table | Supports field projection, filters, pagination |
| queryIndex(args) | Query a secondary index | Supports hash key, optional range condition, filters, projection, pagination |
CRUD Example
const created = await userRepo.put({
id: 'user-1',
username: 'admin',
roles: ['ROLE_AAA_ADMIN'],
})
const projected = await userRepo.get('user-1', ['id', 'username'])
const updated = await userRepo.update('user-1', {
username: 'super-admin',
})
await userRepo.remove('user-1')Update Semantics
- keys with concrete values are added to the
SETclause - keys explicitly set to
undefinedare moved to theREMOVEclause updatedAtis injected automatically
Scan Example
Use scan() when you do not have a matching index strategy and can tolerate a table scan.
const page = await userRepo.scan({
fields: ['id', 'username'],
filters: [
{ field: 'username', op: 'begins_with', value: 'adm' },
{ field: 'roles', op: 'contains', value: 'ROLE_AAA_ADMIN' },
],
Limit: 20,
})Supported filter operators:
=<><<=>>=betweenbegins_withcontainsinattribute_existsattribute_not_exists
queryIndex() Examples
Use queryIndex() when you know the GSI or LSI to query and you can provide the hash key.
Simple token lookup
const tokens = await tokenRepo.queryIndex({
index: 'token-index',
hash: {
field: 'token',
value: 'test-admin-token-12345',
},
limit: 1,
})Hash key, range key, filters, projection, and pagination
const leads = await leadRepo.queryIndex({
index: 'source-createdAt-index',
hash: {
field: 'source',
value: 'source-1',
},
range: {
field: 'createdAt',
op: '>=',
value: 1_710_000_000,
},
fields: ['id', 'phoneNumber', 'createdAt'],
filters: [
{ field: 'transmissionEnabled', op: '=', value: true },
],
limit: 50,
scanIndexForward: false,
})Composite-Key Example With keyOf
type Session = {
pk: string
sk: string
userId: string
status: 'active' | 'closed'
}
const sessionRepo = createRepo<Session>({
baseTableName: 'sessions',
id: {
keyOf: ({ userId, sk }: { userId: string; sk: string }) => ({
pk: `USER#${userId}`,
sk,
}),
},
})
const session = await sessionRepo.get({ userId: 'user-1', sk: 'SESSION#1' })Expression Helpers
The expression helpers are useful when you want to keep using the AWS SDK directly but avoid rebuilding expression strings by hand.
import {
buildFilterExpression,
buildKeyCondition,
buildProjection,
buildUpdate,
} from '@verisure-italy/dynamo-kit'
const projection = buildProjection(['id', 'username'])
const update = buildUpdate({ username: 'new-name', description: undefined })
const keyCondition = buildKeyCondition({
hash: { field: 'source', value: 'source-1' },
range: { field: 'createdAt', op: 'between', value: [100, 200] },
})
const filters = buildFilterExpression([
{ field: 'enabled', op: '=', value: true },
])Notes
physicalTableName()applies the configured table prefix without duplicating naming logic in consumers.- Pagination
nextTokenvalues are raw DynamoDB keys. queryIndex()still returns aPage<TEntity>even when you query by a secondary index.scan()andqueryIndex()returnLastEvaluatedKeyasnextTokenwithout additional abstraction.
