entity-manager
The DynamoDB single-table design pattern requires highly-structured key attributes that...
- Serve as Global Secondary Index (GSI) keys supporting entity-specific queries.
- Support sharding across multiple partitions.
Entity relationships in a traditional RDBMS are expressed in a set of Foreign Key constraints that map straightforwardly into an entity-relationship diagram (ERD). Keeping implementation and design in sync is painless, and scaling is a matter of hardware and server configuration.
All of this takes place at design time. At run time, database structure is more or less fixed.
GSIs on a NoSQL platform like DynamoDB are declared as a matter of design-time configuration. Everything ELSE happens at run time, encoded into those critical key attributes at every data write.
The logic to accomplish this can be both complex and fragmented across the implementation. Unlike a set of RDBMS foreign key constraints that map one-for-one to the lines in an ERD, the structure of this logic is difficult to visualize and rarely collected in one place.
This package shifts the implementation of DynamoDB structure & scaling from logic to configuration. It features:
-
A simple, declarative configuration format that permits articulation of every index key for every entity, all in one place.
-
A rational approach to partition sharding that can be encoded directly into table & GSI hash keys and permits scaling over time.
-
High-performance decoration of entity-specific data objects with configured, shard-aware index values.
-
High-performance transformation of an entity-specific data object into a key space permitting query of related objects across all partition shards.
Installation
npm install @karmaniverous/entity-manager
Configuration
The entity-manager
configuration object describes each entity's structured keys and sharding strategy. For broadest compatibility, express this object as a named export from an ES6 module like this:
import { sn2e, sn2u } from '@karmaniverous/tagged-templates';
export const config = {
entities: {
transaction: {
keys: {
entityPK: ({ shardId }) => `transaction${sn2e`!${shardId}`}`,
transactionSK: ({ timestamp, transactionId }) =>
sn2u`timestamp#${timestamp}|transactionId#${transactionId}`,
merchantPK: ({ merchantId, shardId }) =>
sn2u`merchantId#${merchantId}|transaction${sn2e`!${shardId}`}`,
merchantSK: ({ methodId, timestamp, transactionId }) =>
sn2u`timestamp#${timestamp}|methodId#${methodId}|transactionId#${transactionId}`,
methodPK: ({ methodId, shardId }) =>
sn2u`method#${methodId}|transaction${sn2e`!${shardId}`}`,
methodSK: ({ merchantId, timestamp, transactionId }) =>
sn2u`timestamp#${timestamp}|merchantId#${merchantId}|transactionId#${transactionId}`,
userPK: ({ shardId, userId }) =>
sn2u`user#${userId}|transaction${sn2e`!${shardId}`}`,
userSK: ({ merchantId, timestamp, transactionId }) =>
sn2u`timestamp#${timestamp}|merchantId#${merchantId}|transactionId#${transactionId}`,
},
sharding: {
nibbles: 0,
nibbleBits: 3,
bumps: {
1676874972686: 1,
1708411134487: 2,
},
entityKey: ({ transactionId }) => transactionId,
timestamp: ({ timestamp }) => timestamp,
},
},
},
shardKeyToken: 'shardId',
};
Usage
import { Logger } from '@karmaniverous/edge-logger';
const logger = new Logger('debug');
import { EntityManager } from '@karmaniverous/entity-manager';
import { config } from './entityConfig.js';
const entityManager = new EntityManager({ config, logger });
const transaction = {
methodId: 'methodIdValue',
merchantId: 'merchantIdValue',
timestamp: 1676869312851,
transactionId: 'transactionIdValue',
userId: 'userIdValue',
};
entityManager.addKeys(transaction);
entityManager.getKeySpace('transaction', transaction, 'userPK', 1686874972686);
See unit tests for more usage examples.
Future-Proofing
The current design provides for scaling via planned increases in shard key length. The number of shards per key character does not need to be decided until shard keys are first applied.
This design assumes that currently-defined key structures will remain stable across the life of the database, meaning new ones could be layered on but existing ones should not be changed once in use.
The same technique that provides for shard key length bumps could also be applied to such schema changes, permitting unified query across schema changes in the same manner as the package currently supports unified query across shards.
This change can be accomplished with no breaking changes to existing implementations.
API Documentation
entity-manager
- entity-manager
- static
- .EntityManager
- new exports.EntityManager(options)
- .addKeys(entityToken, item, [overwrite]) ⇒
object
- .calcShardKey(entityToken, item) ⇒
string
- .dehydrateIndex(entityToken, indexToken, index, [delimiter]) ⇒
string
- .getKey(entityToken, keyToken) ⇒
object
- .getKeySpace(entityToken, keyToken, item, timestamp) ⇒
Array.<string>
- .query(options) ⇒
Promise.<ShardedQueryResult>
- .rehydrateIndex(entityToken, indexToken, value, [delimiter]) ⇒
object
- .removeKeys(entityToken, item) ⇒
object
- inner
entity-manager.EntityManager
Manage DynamoDb entities.
Kind: static class of entity-manager
- .EntityManager
- new exports.EntityManager(options)
- .addKeys(entityToken, item, [overwrite]) ⇒
object
- .calcShardKey(entityToken, item) ⇒
string
- .dehydrateIndex(entityToken, indexToken, index, [delimiter]) ⇒
string
- .getKey(entityToken, keyToken) ⇒
object
- .getKeySpace(entityToken, keyToken, item, timestamp) ⇒
Array.<string>
- .query(options) ⇒
Promise.<ShardedQueryResult>
- .rehydrateIndex(entityToken, indexToken, value, [delimiter]) ⇒
object
- .removeKeys(entityToken, item) ⇒
object
new exports.EntityManager(options)
Create an EntityManager instance.
Returns: EntityManager
- EntityManager instance.
Throws:
Error
If config is invalid.Error
If logger is invalid.
Param | Type | Description |
---|
options | object | Options object. |
[options.config] | object | EntityManager configuration object (see README for a breakdown). |
[options.logger] | object | Logger instance (defaults to console, must support error & debug methods). |
entityManager.addKeys(entityToken, item, [overwrite]) ⇒ object
Add sharded keys to an entity item. Does not mutate original item.
Kind: instance method of EntityManager
Returns: object
- Decorated entity item.
Throws:
Error
If entityToken is invalid.Error
If item is invalid.
Param | Type | Default | Description |
---|
entityToken | string | | Entity token. |
item | object | | Entity item. |
[overwrite] | boolean | false | Overwrite existing properties. |
entityManager.calcShardKey(entityToken, item) ⇒ string
Calculated the shard key for an entity item.
Kind: instance method of EntityManager
Returns: string
- Shard key.
Param | Type | Description |
---|
entityToken | string | Entity token. |
item | object | Entity item. |
entityManager.dehydrateIndex(entityToken, indexToken, index, [delimiter]) ⇒ string
Condense an index object into a delimited string.
Kind: instance method of EntityManager
Returns: string
- Dehydrated index.
Throws:
Error
If entityToken is invalid.Error
If indexToken is invalid.Error
If item is invalid.
Param | Type | Default | Description |
---|
entityToken | string | | Entity token. |
indexToken | string | Array.<string> | | Index token or array of key tokens. |
index | object | | Index object. |
[delimiter] | string | "~" | Delimiter. |
entityManager.getKey(entityToken, keyToken) ⇒ object
Return the config for a given entity key token.
Kind: instance method of EntityManager
Returns: object
- Entity key config.
Param | Type | Description |
---|
entityToken | string | Entity token. |
keyToken | string | Key token. |
entityManager.getKeySpace(entityToken, keyToken, item, timestamp) ⇒ Array.<string>
Return an array of sharded keys valid for a given entity token & timestamp.
Kind: instance method of EntityManager
Returns: Array.<string>
- Array of keys.
Throws:
Error
If entityToken is invalid.Error
If item is invalid.Error
If keyToken is invalid.Error
If timestamp is invalid.
Param | Type | Description |
---|
entityToken | string | Entity token. |
keyToken | string | Key token. |
item | object | Entity item sufficiently populated to generate property keyToken. |
timestamp | number | Timestamp. |
entityManager.query(options) ⇒ Promise.<ShardedQueryResult>
Query an entity across shards.
Kind: instance method of EntityManager
Returns: Promise.<ShardedQueryResult>
- Sharded query result.
Param | Type | Description |
---|
options | object | Query options. |
options.entityToken | string | Entity token. |
[options.keyToken] | string | Key token. |
[options.item] | object | Entity item sufficiently populated to generate property keyToken. |
options.shardQuery | ShardQueryFunction | Sharded query function. |
[options.limit] | number | Request limit. |
[options.pageKeys] | object | Map of shard page keys. |
[options.pageSize] | number | Request page size. |
entityManager.rehydrateIndex(entityToken, indexToken, value, [delimiter]) ⇒ object
Convert a delimited string into a named index key.
Kind: instance method of EntityManager
Returns: object
- Rehydrated index key.
Throws:
Error
If entityToken is invalid.Error
If indexToken is invalid.
Param | Type | Default | Description |
---|
entityToken | string | | Entity token. |
indexToken | string | Array.<string> | | Index token or array of key tokens. |
value | string | | Dehydrated index value. |
[delimiter] | string | "~" | Delimiter. |
entityManager.removeKeys(entityToken, item) ⇒ object
Remove sharded keys from an entity item. Does not mutate original item.
Kind: instance method of EntityManager
Returns: object
- Stripped entity item.
Throws:
Error
If entityToken is invalid.Error
If item is invalid.
Param | Type | Description |
---|
entityToken | string | Entity token. |
item | object | Entity item. |
entity-manager~ShardQueryResult : object
Kind: inner typedef of entity-manager
Properties
Name | Type | Description |
---|
items | Array.<any> | Query result array. |
pageKey | * | Shard page key. |
entity-manager~ShardQueryFunction ⇒ Promise.<ShardQueryResult>
Shard query function
Kind: inner typedef of entity-manager
Returns: Promise.<ShardQueryResult>
- Sharded query result.
Param | Type | Description |
---|
shardedKey | string | Sharded key. |
[pageKey] | * | Page key. |
[limit] | number | Request limit. |
entity-manager~ShardedQueryResult : object
Kind: inner typedef of entity-manager
Properties
Name | Type | Description |
---|
items | Array.<any> | Query result array. |
pageKeys | object | Shard page keys. |
Built with ❤️ on Bali! Find more great tools & templates on my GitHub Profile.