@x-sls/dynamodb-users
v1.2.0
Published
Helpers for managing users in DynamoDB: create, update, get by ID or email, and list users by lookup list (e.g. active/inactive). The library never creates a DynamoDB client; you pass in a DocumentClient and table name.
Readme
@x-sls/dynamodb-users
Helpers for managing users in DynamoDB: create, update, get by ID or email, and list users by lookup list (e.g. active/inactive). The library never creates a DynamoDB client; you pass in a DocumentClient and table name.
Table requirements
- Base table: partition key
pk(String), sort keysk(String). - GSI (default name
gsi1): partition keypk1(String), sort keysk1(String).
If your table uses different key or index names, override them via options.schema (see Schema).
Data model
Each user is represented by:
- User item — Main record:
pk = U#<uuid>,sk = A, pluspk1/sk1for email lookup on the GSI (e.g.pk1 = email#<normalized>,sk1 = A). Get by ID via base table; get by email via GSI query. - Email item — Sentinel for by-email lookup when the GSI is eventually consistent:
pk = EMAIL#<normalized>,sk = A,userId. The library queries the GSI first, then falls back to this item +getUser. - LOOKUP item (optional) — For list queries:
pk = U#<id>,sk = LOOKUP,pk1 = <lookupList>(e.g.USER#ACTIVE),sk1 = <normalized name>. Projected attributes (e.g. email, name, picture, userId). Created/updated whencreateLookupItem/updateLookupItemare not set tofalse.
Installation
npm install @x-sls/dynamodb-usersPeer dependency: @aws-sdk/lib-dynamodb (DocumentClient). You must provide your own DynamoDB client and pass it via options.
API
All functions take an options object that must include documentClient and tableName. Optional schema overrides the default key/index names and prefixes.
getUser(userId, options)
Get a user by ID (with or without U# prefix).
- Returns: User item or
null. - Options:
documentClient,tableName(required);schema(optional).
const user = await getUser('abc-123', { documentClient, tableName });
const user2 = await getUser('U#abc-123', { documentClient, tableName });getUserByEmail(email, options)
Get a user by email. Queries the GSI first; if empty, falls back to the EMAIL sentinel and then getUser.
- Returns: User item or
null. - Options:
documentClient,tableName(required);schema(optional; must havelookupIndexfor by-email);normalizeEmailFn(optional, default: lowercased trim).
const user = await getUserByEmail('[email protected]', { documentClient, tableName });createUser(input, options)
Create a user atomically: user item + email sentinel. By default also writes a LOOKUP item so the user can be returned by listUsers (opt-out with createLookupItem: false).
- Input:
email,name(required). Other keys (e.g.picture,status) are stored on the user item. - Returns: The created user item.
- Throws:
"User with this email already exists"on duplicate email (transaction conditional check). - Options:
documentClient,tableName(required);createLookupItem(defaulttrue);lookupList(optional, default fromschema.defaultLookupListe.g.USER#ACTIVE);lookupExtraAttributes(array of attribute names to add to LOOKUP);extraTransactItems,normalizeEmailFn,schema(optional).
const user = await createUser(
{ email: '[email protected]', name: 'Alice' },
{ documentClient, tableName }
);
// With custom list
await createUser(
{ email: '[email protected]', name: 'Bob' },
{ documentClient, tableName, lookupList: 'USER#INACTIVE' }
);updateUser(userId, updates, options)
Update a user. Writes a version record (if createVersionRecord is true), updates the user item, and on email change updates/deletes/puts email sentinels. By default also (re)writes the LOOKUP item (opt-out with updateLookupItem: false).
- Returns: Updated user item.
- Throws:
"User not found", or"User with this email already exists"if email is changed to an existing one. - Options:
documentClient,tableName,modifiedBy,modifiedReason(required);createVersionRecord(defaulttrue);updateLookupItem(defaulttrue);lookupList(optional);lookupExtraAttributes;allowedUpdateFields(if set, only these keys are updated);extraTransactItems,normalizeEmailFn,schema(optional).
await updateUser(
userId,
{ name: 'Alice Smith' },
{ documentClient, tableName, modifiedBy: 'admin', modifiedReason: 'name change' }
);
// Move to inactive list
await updateUser(
userId,
{},
{ documentClient, tableName, modifiedBy: 'admin', modifiedReason: 'deactivate', lookupList: 'USER#INACTIVE' }
);listUsers(options)
List users by querying the LOOKUP index (GSI). Only returns users for whom LOOKUP items exist (created/updated by this library with LOOKUP enabled). If you never create LOOKUP items, listUsers will return empty results; implement your own list logic in that case.
- Returns:
{ items: object[], lastEvaluatedKey: object | null }. Each item is a LOOKUP row (projected attributes +userId). UselastEvaluatedKeyfor pagination. - Options:
documentClient,tableName,lookupList(required, e.g.'USER#ACTIVE');beginsWith(optional, prefix match on normalized name);limit(default 50, max 100);exclusiveStartKey,scanIndexForward;schema(optional).
const { items, lastEvaluatedKey } = await listUsers({
documentClient,
tableName,
lookupList: 'USER#ACTIVE'
});
// Pagination
const next = await listUsers({
documentClient,
tableName,
lookupList: 'USER#ACTIVE',
exclusiveStartKey: lastEvaluatedKey
});
// Search by name prefix (normalized)
const { items } = await listUsers({
documentClient,
tableName,
lookupList: 'USER#ACTIVE',
beginsWith: 'alice'
});LOOKUP behavior (opt-out)
- createUser: Creates a LOOKUP item by default (
createLookupItemdefaults totrue), so new users appear in the default list (e.g.USER#ACTIVE) and can be listed. SetcreateLookupItem: falseto skip LOOKUP. - updateUser: Updates the LOOKUP item by default (
updateLookupItemdefaults totrue). SetupdateLookupItem: falseto skip LOOKUP. - listUsers: Only returns items that match the LOOKUP pattern (same GSI,
pk1 = lookupList). If you opt out of LOOKUP on create/update, you must implement your own listing (e.g. Scan or custom GSI).
Schema
The library uses a default schema (exported as DEFAULT_SCHEMA). Override specific keys by passing options.schema; it is merged over the default.
| Key | Default | Description |
|-----|---------|-------------|
| userItemPK | 'pk' | Base table partition key attribute name |
| userItemSK | 'sk' | Base table sort key attribute name |
| userItemPrefix | 'U#' | Prefix for user item pk |
| userItemLookupPrefix | 'email#' | Prefix for user item GSI pk1 (email lookup) |
| userItemSKValue | 'A' | User item sk value |
| userItemLookupSKValue | 'A' | User item GSI sk1 value for email lookup |
| lookupIndex | 'gsi1' | GSI name |
| lookupIndexPK | 'pk1' | GSI partition key attribute |
| lookupIndexSK | 'sk1' | GSI sort key attribute |
| lookupItemSK | 'LOOKUP' | LOOKUP item sk value |
| defaultLookupList | 'USER#ACTIVE' | Default list for new users (LOOKUP pk1) |
| emailItemPrefix | 'EMAIL#' | Email sentinel pk prefix |
| emailItemUserAttribute | 'userId' | Attribute name for user ID on email/LOOKUP items |
Example: use U# for the email lookup key instead of email#:
const user = await getUserByEmail('[email protected]', {
documentClient,
tableName,
schema: { userItemLookupPrefix: 'U#' }
});Example: full flow
const { createUser, getUser, getUserByEmail, updateUser, listUsers } = require('@x-sls/dynamodb-users');
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocument } = require('@aws-sdk/lib-dynamodb');
const client = new DynamoDBClient({ region: 'us-east-1' });
const documentClient = DynamoDBDocument.from(client);
const tableName = process.env.USERS_TABLE;
const opts = { documentClient, tableName };
// Create (includes LOOKUP on USER#ACTIVE by default)
const user = await createUser({ email: '[email protected]', name: 'Alice' }, opts);
// Get by ID or email
const byId = await getUser(user.pk, opts);
const byEmail = await getUserByEmail('[email protected]', opts);
// List active users
const { items } = await listUsers({ ...opts, lookupList: 'USER#ACTIVE' });
// Mark inactive
await updateUser(user.pk, {}, { ...opts, modifiedBy: 'system', modifiedReason: 'deactivate', lookupList: 'USER#INACTIVE' });
// List inactive users
const { items: inactive } = await listUsers({ ...opts, lookupList: 'USER#INACTIVE' });Tests
npm testUses Node's built-in test runner (node --test).
