Continuing our earlier example from step by step guide, number of things happen before item is actually saved in table.
const user = new User();
user.name = 'Loki';
user.status = 'active';
user.email = '[email protected]'
{
TransactionItems : [
{
Put: {
Item: {
PK: 'USER#0000-111-21ssa2-1111',
SK: 'USER#0000-111-21ssa2-1111',
GSI1PK: 'USER#0000-111-21ssa2-1111',
GSI1SK: 'USER#0000-111-21ssa2-1111#STATUS#active',
id: '0000-111-21ssa2-1111',
name: 'Loki',
status: 'active',
email: '[email protected]',
updatedAt: 1600009090
},
ConditionExpression: 'attribute_not_exists(#CE_PK)',
ExpressionAttributeNames: {
'#CE_PK': 'PK',
},
TableName: 'my-table'
}
}, {
Put: {
Item: {
PK: 'DRM_GEN_USER.EMAIL#[email protected]',
SK: 'DRM_GEN_USER.EMAIL#[email protected]',
},
ConditionExpression: 'attribute_not_exists(#CE_PK)',
ExpressionAttributeNames: {
'#CE_PK': 'PK',
},
TableName: 'my-table'
}
}
]
}
Here is the detailed explanation to what steps TypeDORM takes to generate above item input, these are the some of many steps TypeDORM takes to transform simple dev friendly model to dynamoDB friendly entity.
First of all, we fetch all the indexes, key configuration for given entity, in this case User
, after this step our simple user model looks like this:
{
primaryKey: {
PK: 'USER#{{id}}',
SK: 'USER#{{id}}'
},
GSI1: {
GSI1PK: 'USER#{{id}}',
GSI1SK: 'USER#{{id}}#STATUS#{{status}}'
type: 'GSI'
},
id: '0000-111-21ssa2-1111',
name: 'Loki',
status: 'active',
email: '[email protected]',
updatedAt: 1600009090
}
Next step is to replace all interpolations in schema with actual value, typeDORM looks for {{ }}
pattern, and will try to replace anything in between with matching property on object, once this is done, item to create looks something like this.
{
PK: 'USER#0000-111-21ssa2-1111',
SK: 'USER#0000-111-21ssa2-1111',
GSI1PK: 'USER#0000-111-21ssa2-1111',
GSI1SK: 'USER#0000-111-21ssa2-1111#STATUS#active',
id: '0000-111-21ssa2-1111',
name: 'Loki',
status: 'active',
email: '[email protected]',
updatedAt: 1600009090
}
Since TypeDORM uses Document client to communicate with dynamoDB, it needs to transform item to document client input object. which will end up, looking something like this.
{
Item: {
PK: 'USER#0000-111-21ssa2-1111',
SK: 'USER#0000-111-21ssa2-1111',
GSI1PK: 'USER#0000-111-21ssa2-1111',
GSI1SK: 'USER#0000-111-21ssa2-1111#STATUS#active',
id: '0000-111-21ssa2-1111',
name: 'Loki',
status: 'active',
email: '[email protected]',
updatedAt: 1600009090
},
ConditionExpression: 'attribute_not_exists(#CE_PK)',
ExpressionAttributeNames: {
'#CE_PK': 'PK',
},
TableName: 'my-table'
}
Looks familiar right, but We are not done yet, email
attribute on User
, is marked as to be made unique, and to make this possible, TypeDORM follows a unique attribute design pattern, where attributes marked as unique will be created as a separate record, and both original and unique records will be written to table as a single transaction. So final item input to document client will look like this:
{
TransactionItems : [
{
Put: {
Item: {
PK: 'USER#0000-111-21ssa2-1111',
SK: 'USER#0000-111-21ssa2-1111',
GSI1PK: 'USER#0000-111-21ssa2-1111',
GSI1SK: 'USER#0000-111-21ssa2-1111#STATUS#active',
id: '0000-111-21ssa2-1111',
name: 'Loki',
status: 'active',
email: '[email protected]',
updatedAt: 1600009090
},
ConditionExpression: 'attribute_not_exists(#CE_PK)',
ExpressionAttributeNames: {
'#CE_PK': 'PK',
},
TableName: 'my-table'
}
}, {
Put: {
Item: {
PK: 'DRM_GEN_USER.EMAIL#[email protected]',
SK: 'DRM_GEN_USER.EMAIL#[email protected]',
},
ConditionExpression: 'attribute_not_exists(#CE_PK)',
ExpressionAttributeNames: {
'#CE_PK': 'PK',
},
TableName: 'my-table'
}
}
]
}
Because of the way attribute_not_exists constraint and transaction api work, item will only be created if there is not item matching id
(this is already handled by primary key) and email
. Pretty cool right?. 😲
entityManager.update(User, {id: '11'},
{name: 'Ex-Loki', status: 'inactive'}
)
{
ExpressionAttributeNames: {
'#attr0': 'name',
'#attr1': 'status',
'#attr2': 'GSI1SK',
},
ExpressionAttributeValues: {
':val0': 'Ex-Loki',
':val1': 'inactive',
':val2': 'USER#11#STATUS#inactive',
},
Key: {
PK: 'USER#11',
SK: 'USER#11',
},
ReturnValues: 'ALL_NEW',
TableName: 'test-table',
UpdateExpression:
'SET #attr0 = :val0, #attr1 = :val1, #attr2 = :val2, #attr3 = :val3',
}
Here, We only asked to updated name
and status
, but why did GSI1SK
also got updated? It's because in User
entity schema, GSI1SK
references status
attribute, and TypeDORM can infer that, meaning we are no longer required to also having to maintain indexes when value for any of the attributes changes.
Querying in dynamo db can be cumbersome, but it doesn't have to be that way. TypeDORM takes a simple approach of generating query expressions from given query options.
entityManager.find(Order,
{userId: 'user-1'}, {
queryIndex: 'GSI1',
keyCondition: {
BEGINS_WITH: 'ORDER#cancelled',
},
})
This is the query expression TypeDORM generates for above query condition
{
ExpressionAttributeNames: {
'#KY_CE_GSI1PK': 'GSI1PK',
'#KY_CE_GSI1SK': 'GSI1SK',
},
ExpressionAttributeValues: {
':KY_CE_GSI1PK': 'USER#user-1',
':KY_CE_GSI1SK': 'ORDER#cancelled',
},
KeyConditionExpression:
'#KY_CE_GSI1PK = :KY_CE_GSI1PK AND begins_with(#KY_CE_GSI1SK, :KY_CE_GSI1SK)',
ScanIndexForward: true,
IndexName: 'GSI1',
TableName: 'test-table',
}
Deleting an item can only be done on by specifying item's primary key, it will simply look as following
entityManager.delete(User, {id: 'user-1'})
{
Key: {
PK: 'USER#user-1',
SK: 'USER#user-1',
},
TableName: 'test-table',
}
Best of all, TypeDORM does all of these without coming in the way of developer, unlike other ORM tools, where design patterns are assumed.