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 as organization but want to save the attribute with a field name of org 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 on set

Attribute Options

PropertyTypeRequiredTypesDescription
typestring, ReadonlyArray<string>, string[]yesallAccepts 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.
requiredbooleannoallFlag 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.
hiddenbooleannoallFlag an attribute as hidden to remove the property from results before they are returned.
defaultvalue, () => valuenoallEither 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
validateRegExp, (value: any) => void, (value: any) => stringnoallEither 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.
fieldstringnoallThe 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.
readOnlybooleannoallPrevents 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.
labelstringnoallUsed in index key composition to prefix key composite attributes. By default, the AttributeName is used as the label.
padding{ length: number; char: string; }nostring, numberSimilar 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) => valuenoallA 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) => valuenoallA 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.
watchAttribute[], "*"noroot-onlyDefine 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*mapDefine 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.
itemsAttributeyes*listDefine 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.
itemsstring, numberyes*setDefine 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 or create operation, or is changed via a patch or updated operation, the Attribute’s set 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:

  1. If a field exists on an item after retrieval from DynamoDB, the attribute associated with that field will have its getter method invoked.
  2. After a put or create operation is performed, attribute getters are applied against the object originally received and returned.
  3. 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:

  1. Setters for all Attributes will always be invoked when performing a create or put operation.
  2. Setters will only be invoked when an Attribute is modified when performing a patch,update, or upsert operation.
  3. 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, the watch 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 cannot watch another attribute that also uses watch, so ElectroDB first invokes the getters or setters of attributes without the watch property, then subsequently invokes the getters or setters of attributes who use watch.

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:

signaturebehavior
RegexpElectroDB will call .test(val) on the provided regex with the value passed to this attribute
(value: T) => stringIf 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) => booleanIf 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) => voidA 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 },
);

Try it out!