Prerequisite reading - the very first part of Setup on installing the library.
Entity objects are used by DynamoDB Entity Store during all database operations for defining configuration and behavior.
Typically Entity objects are defined as global constants within your app, only needing to be instantiated once per Entity type.
Each Entity object must satisfy the Entity
interface :
interface Entity<TItem extends TPKSource & TSKSource, TPKSource, TSKSource> {
type: string
pk(source: TPKSource): NativeScalarAttributeValue
sk(source: TSKSource): NativeScalarAttributeValue
convertToDynamoFormat?: EntityFormatter<TItem>
parse: EntityParser<TItem>
gsis?: Record<string, GsiGenerators>
}
Every Entity must satisfy the type signature Entity<TItem extends TPKSource & TSKSource, TPKSource, TSKSource>
.
TItem
is intended to be the "internal" type of the object that you're persisting to DynamoDB.
TItem
is both the type of the objects you are writing (e.g. using put
) and the result of the items that you are reading (e.g. using get
or query
).
For example you may have an "internal" type like this:
interface Sheep {
breed: string
name: string
ageInYears: number
}
Your corresponding Entity object would be of type Entity<Sheep,...>
.
TPKSource
and TSKSource
are the input types used for generating Partition Key and Sort Key values. DynamoDB Entity Store needs these values whenever it is writing an object with put
, and whenever else it needs to specify a key. TPKSource
and TSKSource
must each be a subset of your overall "internal" type.
If your table only has a Partition Key, and doesn't have a Sort Key, see the section PK-only Entities below.
With our example above, if the Partition Key for persisted sheep is based on the breed
field, then TPKSource
is:
type SheepPKSource = {
breed: string
}
and if the Sort Key for persisted sheep is based on the name
field, then TSKSource
is:
type SheepSKSource = {
name: string
}
However, since we know that both the PK source and SK Source types are subsets of Sheep
, we can instead define the types using TypeScript Pick
utility type:
type SheepPKSource = Pick<Sheep, 'breed'>
type SheepSKSource = Pick<Sheep, 'name'>
We can then build our entire "Sheep Entity" type :
Entity<Sheep, Pick<Sheep, 'breed'>, Pick<Sheep, 'name'>>
The type
field must be a string, unique for each type of Entity you access with an instance DynamoDB Entity Store. This value is used in a number of ways:
- Written as an attribute whenever you
put
an object, unless you configure not to do so at the table level. The default attribute name is_et
(for "Entity Type"), but this is also configurable (see previous chapter for changing how and whether the entity type is automatically written to the table). - To filter results during query and scan operations (unless configured otherwise).
- For logging, and error messages.
Each Entity must implement two functions which are used to generate Partition Key and Sort Key values.
This occurs during many operations, including put
and get
.
If your table only has a Partition Key, and doesn't have a Sort Key, see the section PK-only Entities below.
Each of pk()
and sk()
is passed an argument of type TPKSource
or TSKSource
, as defined earlier, and should return a "scalar" value (specifically NativeScalarAttributeValue
).
Let's go back to our example of Sheep
from earlier. Let's say we have a particular sheep object that is internally represented as follows:
{ breed: 'merino', name: 'shaun', ageInYears: 3 }
And let's say we'd like to store the following for our key attributes for such an object:
- Partition Key:
SHEEP#BREED#merino
- Sort Key:
NAME#shaun
Our pk()
and sk()
functions are then as follows:
function pk({ breed }: Pick<Sheep, 'breed'>) {
return `SHEEP#BREED#${breed}`
}
function sk({ name }: Pick<Sheep, 'name'>) {
return `NAME#${name}`
}
This example uses destructuring syntax, but that's optional.
Notice that the parameter types here are precisely the same as those we gave for TPKSource
and TSKSource
in the Entity type definition.
A couple of less common scenarios.
First - it's usually the case that your pk()
and sk()
functions return a context-free value based on their parameter types - TPKSource
/ TSKSource
.
In such cases your entity will likely be a stateless object.
Sometimes though the value you want to generate for pk()
and/or sk()
will be partly or solely dependent on some other value(s) - e.g. a configuration value.
In these situations you may need to create a stateful entity object (which you pass to entity store in for(entity)
.
Second - if either of your Partition Key or Sort Key attributes are also being used to store specific fields of your entity (in other words your table does not have separate PK
and SK
style attributes configured) then you can just return field values unmanipulated from your generator functions, but you still need to implement the functions .
E.g. say you have an internal type as follows:
interface Farm {
name: string
address: string
}
and say that your DynamoDB table only stores farms. In such a case you might choose to use the name
field as the actual partition key. In such a case the pk()
generator would be as follows:
function pk({ name }: Pick<Farm, 'name'>) {
return name
}
convertToDynamoFormat()
is an optional function you may choose to implement in order to change how DynamoDB Entity Store writes an object to DynamoDB during put
operations.
Since DynamoDB Entity Store uses the AWS Document Client library under the covers, this is more about choosing which fields to save, and any field-level modification, rather than lower-level "marshalling".
If you need to change marshalling options at the AWS library level please refer to the Setup chapter.
By default DynamoDB Entity Store will store all the fields of an object, unmanipulated, using the field names of the object. E.g. going back to our Sheep
example, let's say we're writing the following object:
{ breed: 'merino', name: 'shaun', ageInYears: 3 }
This might result (depending on table configuration) in the following object being written to DynamoDB:
PK |
SK |
breed |
name |
ageInYears |
_et |
_lastUpdated |
---|---|---|---|---|---|---|
SHEEP#BREED#merino |
NAME#shaun |
merino |
shaun |
3 | sheep |
2023-08-21T15:41:53.566Z |
PK
and SK
come from calling the pk()
and sk()
generator functions, _et
comes from the .entityType
field, and _lastUpdated
is the current date and time. The remaining fields - breed
, name
, and ageInYears
- are simply a duplication of the original object.
The metadata fields - PK
, SK
, _et
, _lastUpdated
- are controlled through other mechanisms, but if you want to change what data fields are stored, and what values are stored for those fields, then you must implement convertToDynamoFormat()
.
The type signature of convertToDynamoFormat()
is: (item: TItem) => DynamoDBValues
, in other words it receives an object of your "internal" type, and must return a valid DynamoDB Document Client object (DynamoDBValues
is an alias for Record<string, NativeAttributeValue>
, where NativeAttributeValue
comes from the AWS library.)
You may need to implement convertToDynamoFormat()
in situations like the following:
- You don't want to persist all of the fields on your internal object
- You want to persist some or all fields with attribute names different from the internal object field names
- You want to store attributes not present on the internal object but that are available from a larger context
- You want to change the format of persisted values before storing them - e.g. changing a numeric Date field to a ISO string value, or changing a nested structure into an encoded form.
If you implement convertToDynamoFormat()
you'll likely also need to consider a non-default implementation of parse()
, which is discussed next.
Each Entity's parse()
function is used during read operations to convert the DynamoDB-persisted version of an item to the "internal" version of an item. As with .convertToDynamoFormat()
, since DynamoDB Entity Store uses the AWS Document Client library under the covers such parsing is less about low-level type manipulation and more about field selection and calculation.
As described above for .convertToDynamoFormat()
- with DynamoDB Entity Store's default behavior the persisted version of object contains precisely the same fields as the internal version, and so in that case parsing consists of (a) removing all of the metadata fields and (b) validating the type, returning a type-safe value.
The standard, and most simple case, is that you just need to implement a TypeScript Type Predicate. This is a function that validates the correct fields for a type are present.
Going back to our Sheep example, we can define a Type Predicate as follows:
const isSheep = function (x: DynamoDBValues): x is Sheep {
const candidate = x as Sheep
return candidate.breed !== undefined && candidate.name !== undefined && candidate.ageInYears !== undefined
}
If we wanted we could actually be more precise here by checking the actual values are in the correct ranges.
To use the Type Predicate in our Entity
definition we can use the typePredicateParser
helper function. This helper function returns a parser that removes all the metadata fields, and then calls your type predicate, returning the type of your internal object.
Our parse()
implementation for SHEEP_ENTITY
is then defined by calling typePredicateParser(isSheep, 'sheep')
.
If just performing a type check isn't sufficient for an Entity, then you need to implement a custom EntityParser<TItem>
function. EntityParser
is defined as follows:
type EntityParser<TItem> = (
item: DynamoDBValues,
allMetaAttributeNames: string[],
metaAttributeNames: MetaAttributeNames
) => TItem
In other words:
- Given the source item in DynamoDB format...
- ... and the attribute names of all the metadata fields
- ... return an object of the correct internal format
The gsis
field defines generator functions for all of the Global Secondary Indexes (GSIs) an Entity uses. In other words it's like pk()
and sk()
, but for GSIs instead of a table. If an Entity doesn't use GSIs it can leave this field undefined.
The type of gsis
is Record<string, GsiGenerators>
, a map from a GSI identifier to a GSI PK generator, and optionally a GSI SK generator.
The GSI identifier will typically be the same as, or similar to, the name of your actual DynamoDB GSI. The mapping from Entity GSI ID to DynamoDB GSI Name is configured in Table Setup, but as an example the "standard" configuration uses gsi
as the Entity GSI ID, and GSI
for the corresponding index name.
If you understand the table pk()
and sk()
generators then you'll understand the GSI Generators too. See the example in the project README for an example.
If your GSI doesn't have a sort key then you don't need to define an sk()
function on the corresponding GSI generator.
If a table has a GSI, but there is no corresponding field under .gsis
in an Entity using that table, then no GSI key attribute values are written for that Entity.
If a table only has a Partition Key and does not have a sort key then obviously it doesn't make sense to have a sort key type on the Entity, or an sk()
generator function, for any Entity items stored in that table.
In such a case your Entity / Entities can instead implement the PKOnlyEntity
type, and use the entityFromPkOnlyEntity
support function.
See the Farms example to see an example.
There are various Entity-related support functions, which you can see in the entitySupport.ts module.
For several examples, see the examples directory , but for quick reference here's the complete SHEEP_ENTITY
example that I used in this page:
interface Sheep {
breed: string
name: string
ageInYears: number
}
const isSheep = function (x: DynamoDBValues): x is Sheep {
const candidate = x as Sheep
return candidate.breed !== undefined && candidate.name !== undefined && candidate.ageInYears !== undefined
}
export const SHEEP_ENTITY: Entity<Sheep, Pick<Sheep, 'breed'>, Pick<Sheep, 'name'>> = {
type: 'sheep',
parse: typePredicateParser(isSheep, 'sheep'),
pk({ breed }: Pick<Sheep, 'breed'>) {
return `SHEEP#BREED#${breed}`
},
sk({ name }: Pick<Sheep, 'name'>) {
return `NAME#${name}`
}
}