Attributes
Attributes are defined on your entity’s schema.
By default, attributes values will be saved to DynamoDB with the same name as your attribute’s key. Using the attribute
field
property, you can map a different field name in your table. This can be useful to utilize existing tables, existing models, or even to reduce record sizes via shorter field names. For example, you may refer to an attribute asorganization
but want to save the attribute with a field name oforg
in DynamoDB.
Attribute Definition
Use the expanded syntax build out more robust attribute options.
{
type: "string" | "number" | "boolean" | "list" | "map" | "set" | "any" | ReadonlyArray<string>;
required?: boolean;
default?: <type> | (() => <type>);
validate?: RegExp | ((value: <type>) => void | string);
field?: string;
readOnly?: boolean;
label?: string;
cast?: "number"|"string"|"boolean";
get?: (attribute: <type>, schema: any) => <type> | void | undefined;
set?: (attribute?: <type>, schema?: any) => <type> | void | undefined;
watch?: "*" | string[];
padding?: {
length: number;
char: string;
}
}
When using get/set in TypeScript, be sure to use the
?:
syntax to denote an optional attribute onset
Attribute Options
Property | Type | Required | Types | Description |
---|---|---|---|---|
type | string , ReadonlyArray<string> , string[] | yes | all | Accepts the values: "string" , "number" "boolean" , "map" , "list" , "set" , an array of strings representing a finite list of acceptable values: ["option1", "option2", "option3"] , or "any" which disables value type checking on that attribute. |
required | boolean | no | all | Flag an attribute as required to be present when creating a record. This attribute also acts as a type of NOT NULL flag, preventing it from being removed directly. When applied to nested properties, be mindful that default map values can cause required child attributes to fail validation. |
hidden | boolean | no | all | Flag an attribute as hidden to remove the property from results before they are returned. |
default | value , () => value | no | all | Either the default value itself or a synchronous function that returns the desired value. Applied before set and before required check. In the case of nested attributes, default values will apply defaults to children attributes until an undefined value is reached |
validate | RegExp , (value: any) => void , (value: any) => string | no | all | Either regex or a synchronous callback to return an error string (will result in exception using the string as the error’s message), or thrown exception in the event of an error. |
field | string | no | all | The name of the attribute as it exists in DynamoDB, if named differently in the schema attributes. Defaults to the AttributeName as defined in the schema. |
readOnly | boolean | no | all | Prevents an attribute from being updated after the record has been created. Attributes used in the composition of the table’s primary Partition Key and Sort Key are read-only by default. The one exception to readOnly is for properties that also use the watch property, read attribute watching for more detail. |
label | string | no | all | Used in index key composition to prefix key composite attributes. By default, the AttributeName is used as the label. |
padding | { length: number; char: string; } | no | string, number | Similar to label , this property only impacts the attribute’s value during index key composition. Padding allows you to define a string pattern to left pad your attribute when ElectroDB builds your partition or sort key. This can be helpful to implementing zero-padding patterns with numbers and strings in sort keys. Note, this will not impact your attribute’s stored value, if you want to transform the attribute’s field value, use the set callback described below. |
set | (attribute, schema) => value | no | all | A synchronous callback allowing you to apply changes to a value before it is set in params or applied to the database. First value represents the value passed to ElectroDB, second value are the attributes passed on that update/put |
get | (attribute, schema) => value | no | all | A synchronous callback allowing you to apply changes to a value after it is retrieved from the database. First value represents the value passed to ElectroDB, second value are the attributes retrieved from the database. |
watch | Attribute[], "*" | no | root-only | Define other attributes that will always trigger your attribute’s getter and setter callback after their getter/setter callbacks are executed. Only available on root level attributes. |
properties | {[key: string]: Attribute} | yes* | map | Define the properties available on a "map" attribute, required if your attribute is a map. Syntax for map properties is the same as root level attributes. |
items | Attribute | yes* | list | Define the attribute type your list attribute will contain, required if your attribute is a list. Syntax for list items is the same as a single attribute. |
items | string , number | yes* | set | Define the primitive type your set attribute will contain, required if your attribute is a set. Unlike lists, a set defines it’s items with a string of either "string" or "number" . |
Attribute Getters and Setters
Using get
and set
on an attribute can allow you to apply logic before and just after modifying or retrieving a field from DynamoDB. Both callbacks should be pure synchronous functions and may be invoked multiple times during one query.
The first argument in an attribute’s get
or set
callback is the value received in the query. The second argument, called "item"
, in an attribute’s is an object containing the values of other attributes on the item as it was given or retrieved. If your attribute uses watch
, the getter or setter of attribute being watched will be invoked before your getter or setter and the updated value will be on the "item"
argument instead of the original.
Using getters/setters on Composite Attributes is not recommended without considering the consequences of how that will impact your keys. When a Composite Attribute is supplied for a new record via a
put
orcreate
operation, or is changed via apatch
orupdated
operation, the Attribute’sset
callback will be invoked prior to formatting/building your record’s keys on when creating or updating a record.
ElectroDB invokes an Attribute’s get
method in the following circumstances:
- If a field exists on an item after retrieval from DynamoDB, the attribute associated with that field will have its getter method invoked.
- After a
put
orcreate
operation is performed, attribute getters are applied against the object originally received and returned. - When using ElectroDB’s attribute watching functionality, an attribute will have its getter callback invoked whenever the getter callback of any “watched” attributes are invoked. Note: The getter of an Attribute Watcher will always be applied after the getters for the attributes it watches.
ElectroDB invokes an Attribute’s set
callback in the following circumstances:
- Setters for all Attributes will always be invoked when performing a
create
orput
operation. - Setters will only be invoked when an Attribute is modified when performing a
patch
,update
, orupsert
operation. - When using ElectroDB’s attribute watching functionality, an attribute will have its setter callback invoked whenever the setter callback of any “watched” attributes are invoked. Note: The setter of an Attribute Watcher will always be applied after the setters for the attributes it watches.
As of ElectroDB
1.3.0
, thewatch
property is only possible for root level attributes. Watch is currently not supported for nested attributes like properties on a “map” or items of a “list”.
Attribute Watching
Attribute watching is a powerful feature in ElectroDB that can be used to solve many unique challenges with DynamoDB. In short, you can define a column to have its getter/setter callbacks called whenever another attribute’s getter or setter callbacks are called. If you haven’t read the section on Attribute Getters and Setters, it will provide you with more context about when an attribute’s mutation callbacks are called.
Because DynamoDB allows for a flexible schema, and ElectroDB allows for optional attributes, it is possible for items belonging to an entity to not have all attributes when setting or getting records. Sometimes values or changes to other attributes will require corresponding changes to another attribute. Sometimes, to fully leverage some advanced model denormalization or query access patterns, it is necessary to duplicate some attribute values with similar or identical values. This functionality has many uses; below are just a few examples of how you can use watch
:
Using the
watch
property impacts the order of which getters and setters are called. You cannotwatch
another attribute that also useswatch
, so ElectroDB first invokes the getters or setters of attributes without thewatch
property, then subsequently invokes the getters or setters of attributes who usewatch
.
myAttr: {
type: "string",
watch: ["otherAttr"],
set: (myAttr, {otherAttr}) => {
// Whenever "myAttr" or "otherAttr" are updated from an `update` or `patch` operation, this callback will be fired.
// Note: myAttr or otherAttr could be independently undefined because either attribute could have triggered this callback
},
get: (myAttr, {otherAttr}) => {
// Whenever "myAttr" or "otherAttr" are retrieved from a `query` or `get` operation, this callback will be fired.
// Note: myAttr or otherAttr could be independently undefined because either attribute could have triggered this callback.
}
}
Attribute Watching: Watch All
If your attributes need to watch for any changes to an item, you can model this by supplying the watch property a string value of "*"
myAttr: {
type: "string",
watch: "*", // <- "watch all"
set: (myAttr, allAttributes) => {
// Whenever an `update` or `patch` operation is performed, this callback will be fired.
// Note: myAttr or the attributes under `allAttributes` could be independently undefined because either attribute could have triggered this callback
},
get: (myAttr, allAttributes) => {
// Whenever a `query` or `get` operation is performed, this callback will be fired.
// Note: myAttr or the attributes under `allAttributes` could be independently undefined because either attribute could have triggered this callback
}
}
Attribute Validation
The validation
property allows for multiple function/type signatures. Here the different combinations ElectroDB supports:
signature | behavior |
---|---|
Regexp | ElectroDB will call .test(val) on the provided regex with the value passed to this attribute |
(value: T) => string | If a string value with length is returned, the text will be considered the reason the value is invalid. It will generate a new exception this text as the message. |
(value: T) => boolean | If a boolean value is returned, true or truthy values will signify than a value is invalid while false or falsey will be considered valid. |
(value: T) => void | A void or undefined value is returned, will be treated as successful, in this scenario you can throw an Error yourself to interrupt the query |
Attribute Types
Primitive Attributes
ElectroDB supports the following primitive attribute types:
string
number
boolean
Primitive attributes are the only type that can be used as composite attributes.
Enum Attributes
When using TypeScript, if you wish to also enforce this type make sure to us the as const
syntax. If TypeScript is not told this array is Readonly, even when your model is passed directly to the Entity constructor, it will not resolve the unique values within that array.
This may be desirable, however, as enforcing the type value can require consumers of your model to do more work to resolve the type beyond just the type string
.
Regardless of using TypeScript or JavaScript, ElectroDB will enforce values supplied match the supplied array of values at runtime.
The following example shows the differences in how TypeScript may enforce your enum value:
attributes: {
myEnumAttribute1: {
type: ["option1", "option2", "option3"] // TypeScript enforces as `string[]`
},
myEnumAttribute2: {
type: ["option1", "option2", "option3"] as const // TypeScript enforces as `"option1" | "option2" | "option3" | undefined`
},
myEnumAttribute3: {
required: true,
type: ["option1", "option2", "option3"] as const // TypeScript enforces as `"option1" | "option2" | "option3"`
}
}
Map Attributes
Map attributes leverage DynamoDB’s native support for object-like structures. The attributes within a Map are defined under the properties
property; a syntax that mirrors the syntax used to define root level attributes. You are not limited in the types of attributes you can nest inside a map attribute.
attributes: {
myMapAttribute: {
type: "map",
properties: {
myStringAttribute: {
type: "string"
},
myNumberAttribute: {
type: "number"
}
}
}
}
List Attributes
List attributes model array-like structures with DynamoDB’s List type. The elements of a List attribute are defined using the items
property. Similar to Map properties, ElectroDB does not restrict the types of items that can be used with a list.
attributes: {
myStringList: {
type: "list",
items: {
type: "string"
},
},
myMapList: {
myMapAttribute: {
type: "map",
properties: {
myStringAttribute: {
type: "string"
},
myNumberAttribute: {
type: "number"
}
}
}
}
}
Set Attributes
The Set attribute is arguably DynamoDB’s most powerful type. ElectroDB supports String and Number Sets using the items
property set as either "string"
, "number"
, or an array of strings or numbers. When a ReadonlyArray is provided, ElectroDB will enforce those values as a finite list of acceptable values, similar to an Enum Attribute
In addition to having the same modeling benefits you get with other attributes, ElectroDB also simplifies the use of Sets by removing the need to use DynamoDB’s special createSet
class to work with Sets. ElectroDB Set Attributes accept Arrays, JavaScript native Sets, and objects from createSet
as values. ElectroDB will manage the casting of values to a DynamoDB Set value prior to saving and ElectroDB will also convert Sets back to JavaScript arrays on retrieval.
If you are using TypeScript, Sets are currently typed as Arrays to simplify the type system. Again, ElectroDB will handle the conversion of these Arrays without the need to use
client.createSet()
.
attributes: {
myStringSet: {
type: "set",
items: "string"
},
myNumberSet: {
type: "set",
items: "number"
},
myEnumStringSet: {
type: "set",
items: ["RED", "GREEN", "BLUE"] as const // electrodb will only accept the included strings "RED", "GREEN", and/or "BLUE"
}
myEnumNumberSet: {
type: "set",
items: [1, 2, 3, 4] as const // electrodb will only accept the included numbers 1, 2, 3, or 4
}
}
Any Attributes
If you have the need to avoid ElectroDB’s type validation, you can use the any
type. This attribute type is an escape hatch for edge-case scenarios. The any
attribute can only be used as a root attribute (i.e. any
cannot be nested below map
and list
attribute types).
Custom Attributes
If you have a need for a custom attribute type (beyond those supported by ElectroDB) you can use the export function CustomAttributeType
or OpaquePrimitiveType
. These functions can be passed a generic and that allow you to specify a custom attribute with ElectroDB:
CustomAttributeType
This function allows for a narrowing of ElectroDB’s any
, string
, and number
types. This can be useful for expressing complex attribute types.
Custom Attributes do not not enforce runtime type checks. Use attribute validation to ensure values undergo validation.
The function CustomAttributeType
takes one argument, which is the “base” type of the attribute. For complex objects and arrays, the base object would be “any” but you can also use a base type like “string”, “number”, or “boolean” to accomplish (Opaque Keys)[#opaque-keys] which can be used as Composite Attributes.
In this example we accomplish a complex union type:
import { Entity, CustomAttributeType } from "electrodb";
const table = "workplace_table";
type PersonnelRole =
| {
type: "employee";
startDate: number;
endDate?: number;
}
| {
type: "contractor";
contractStartDate: number;
contractEndDate: number;
};
const person = new Entity(
{
model: {
entity: "personnel",
service: "workplace",
version: "1",
},
attributes: {
id: {
type: "string",
},
role: {
type: CustomAttributeType<PersonnelRole>("any"),
required: true,
},
},
indexes: {
record: {
pk: {
field: "pk",
composite: ["id"],
},
sk: {
field: "sk",
composite: [],
},
},
},
},
{ table },
);