README
ElectroDB
ElectroDB is a DynamoDB library to ease the use of having multiple entities and complex hierarchical relationships in a single DynamoDB table.
Please submit issues/feedback or reach out on Twitter @tinkertamper.
Introducing: The NEW ElectroDB Playground
Try out and share ElectroDB Models, Services, and Single Table Design at electrodb.fun
Features
- Use with your existing tables - ElectroDB simplifies building DocumentClient parameters, so you can use it with existing tables/data.
- Attribute Schema Enforcement - Define a schema for your entities with enforced attribute validation, defaults, types, aliases, and more.
- Easily Compose Hierarchical Access Patterns - Plan and design hierarchical keys for your indexes to multiply your possible access patterns.
- Single Table Entity Segregation - Entities created with ElectroDB will not conflict with other entities when using a single table.
- Simplified Sort Key Condition Querying - Write efficient sort key queries by easily building compose keys.
- Simplified Filter Composition - Easily create complex readable filters for DynamoDB queries without worrying about the implementation of
ExpressionAttributeNames
,ExpressionAttributeValues
, andFilterExpressions
. - Simplified Update Expression Composition - Easily compose type safe update operations without having to format tedious
ExpressionAttributeNames
,ExpressionAttributeValues
, andUpdateExpressions
. - Easily Query Across Entities - Define "collections" to create powerful/idiomatic queries that return multiple entities in a single request.
- Automatic Index Selection - Use
.find()
or.match()
methods to dynamically and efficiently query based on defined sort key structures. - Simplified Pagination API - Use
.page()
to easily paginate through result sets. - Use With Your Existing Solution - If you are already using DynamoDB, and want to use ElectroDB, use custom Composite Attribute Templates to leverage your existing key structures.
- TypeScript Support - Strong TypeScript support for both Entities and Services now in Beta.
- Query Directly via the Terminal - Execute queries against your
Entities
,Services
,Models
directly from the command line. - Stand Up Rest Server for Entities - Stand up a REST Server to interact with your
Entities
,Services
,Models
for easier prototyping.
Turn this
tasks
.patch({
team: "core",
task: "45-662",
project: "backend"
})
.set({ status: "open" })
.add({ points: 5 })
.append({
comments: [{
user: "janet",
body: "This seems half-baked."
}]
})
.where(( {status}, {eq} ) => eq(status, "in-progress"))
.go();
Into This
{
"UpdateExpression": "SET #status = :status_u0, #points = #points + :points_u0, #comments = list_append(#comments, :comments_u0), #updatedAt = :updatedAt_u0, #gsi1sk = :gsi1sk_u0",
"ExpressionAttributeNames": {
"#status": "status",
"#points": "points",
"#comments": "comments",
"#updatedAt": "updatedAt",
"#gsi1sk": "gsi1sk"
},
"ExpressionAttributeValues": {
":status0": "in-progress",
":status_u0": "open",
":points_u0": 5,
":comments_u0": [
{
"user": "janet",
"body": "This seems half-baked."
}
],
":updatedAt_u0": 1630977029015,
":gsi1sk_u0": "$assignments#tasks_1#status_open"
},
"TableName": "your_table_name",
"Key": {
"pk": "$taskapp#team_core",
"sk": "$tasks_1#project_backend#task_45-662"
},
"ConditionExpression": "attribute_exists(pk) AND attribute_exists(sk) AND #status = :status0"
}
Table of Contents
- ElectroDB
- Project Goals
- Installation
- Usage
- Entities and Services
- Entities
- Services
- Building Queries
- Query Chains
- Collection Chains
- Execute Queries
- Query Examples
- Query Options
- Errors:
- No Client Defined On Model
- Invalid Identifier
- Invalid Key Composite Attribute Template
- Duplicate Indexes
- Collection Without An SK
- Duplicate Collections
- Missing Primary Index
- Invalid Attribute Definition
- Invalid Model
- Invalid Options
- Duplicate Index Fields
- Duplicate Index Composite Attributes
- Incompatible Key Composite Attribute Template
- Invalid Index With Attribute Name
- Invalid Collection on Index With Attribute Field Names
- Missing Composite Attributes
- Missing Table
- Invalid Concurrency Option
- Invalid Pages Option
- Invalid Limit Option
- Invalid Attribute
- AWS Error
- Unknown Errors
- Invalid Last Evaluated Key
- No Owner For Pager
- Pager Not Unique
- Examples
- Exported TypeScript Types
- Using ElectroDB With Existing Data
- Electro CLI
- Version 1 Migration
- Coming Soon
Project Goals
ElectroDB focuses on simplifying the process of modeling, enforcing data constraints, querying across entities, and formatting complex DocumentClient parameters. Three important design considerations we're made with the development of ElectroDB:
- ElectroDB should be able to be useful without having to query the database itself [read more].
- ElectroDB should be able to be added to a project that already has been established tables, data, and access patterns [read more].
- ElectroDB should not require additional design considerations on top of those made for DynamoDB, and therefore should be able to be removed from a project at any time without sacrifice.
Installation
Install from NPM
npm install electrodb --save
Usage
Require/import Entity
and/or Service
from electrodb
:
const {Entity, Service} = require("electrodb");
// or
import {Entity, Service} from "electrodb";
Entities and Services
To see full examples of ElectroDB in action, go to the Examples section.
Entity
allows you to create separate and individual business objects in a DynamoDB table. When queried, your results will not include other Entities that also exist the same table. This allows you to easily achieve single table design as recommended by AWS. For more detail, read Entities.
Service
allows you to build relationships across Entities. A service imports Entity Models, builds individual Entities, and creates Collections to allow cross Entity querying. For more detail, read Services.
You can use Entities independent of Services, you do not need to import models into a Service to use them individually. However, If you intend to make queries that join
or span multiple Entities you will need to use a Service.
Entities
In ElectroDB an Entity
is represents a single business object. For example, in a simple task tracking application, one Entity could represent an Employee and or a Task that is assigned to an employee.
Require or import Entity
from electrodb
:
const {Entity} = require("electrodb");
// or
import {Entity} from "electrodb";
When using TypeScript, for strong type checking, be sure to either add your model as an object literal to the Entity constructor or create your model using const assertions with the
as const
syntax.
Services
In ElectroDB a Service
represents a collection of related Entities. Services allow you to build queries span across Entities. Similar to Entities, Services can coexist on a single table without collision. You can use Entities independent of Services, you do not need to import models into a Service to use them individually. However, you do you need to use a Service if you intend make queries that join
multiple Entities.
Require:
const {Service} = require("electrodb");
// or
import {Service} from "electrodb";
TypeScript Support
Previously it was possible to generate type definition files (.d.ts
) for you Models, Entities, and Services with the Electro CLI. New with version 0.10.0
is TypeScript support for Entities and Services.
As of writing this, this functionality is still a work in progress, and enforcement of some of ElectroDB's query constraints have still not been written into the type checks. Most notably are the following constraints not yet enforced by the type checker, but are enforced at query runtime:
- Sort Key Composite Attribute order is not strongly typed. Sort Key Composite Attributes must be provided in the order they are defined on the model to build the key appropriately. This will not cause an error at query runtime, be sure your partial Sort Keys are provided in accordance with your model to fully leverage Sort Key queries. For more information about composite attribute ordering see the section on Composite Attributes.
- Put/Create/Update/Patch/Delete/Create operations that partially impact index composite attributes are not statically typed. When performing a
put
orupdate
type operation that impacts a composite attribute of a secondary index, ElectroDB performs a check at runtime to ensure all composite attributes of that key are included. This is detailed more in the section Composite Attribute and Index Considerations. - Use of the
params
method does not yet return strict types. - Use of the
raw
orincludeKeys
query options do not yet impact the returned types.
If you experience any issues using TypeScript with ElectroDB, your feedback is very important, please create a GitHub issue, and it can be addressed.
See the section Exported TypeScript Types to read more about the useful types exported from ElectroDB.
TypeScript Services
New with version 0.10.0
is TypeScript support. To ensure accurate types with, TypeScript users should create their services by passing an Object literal or const object that maps Entity alias names to Entity instances.
const table = "my_table_name";
const employees = new Entity(EmployeesModel, { client, table });
const tasks = new Entity(TasksModel, { client, table });
const TaskApp = new Service({employees, tasks});
The property name you assign the entity will then be "alias", or name, you can reference that entity by through the Service. Aliases can be useful if you are building a service with multiple versions of the same entity or wish to change the reference name of an entity without impacting the schema/key names of that entity.
Services take an optional second parameter, similar to Entities, with a client
and table
. Using this constructor interface, the Service will utilize the values from those entities, if they were provided, or be passed values to override the client
or table
name on the individual entities.
Not yet available for TypeScript, this pattern will also accept Models, or a mix of Entities and Models, in the same object literal format.
Join
When using JavaScript, use join
to add Entities or Models onto a Service.
NOTE: If using TypeScript, see Joining Entities at Service construction for TypeScript to learn how to "join" entities for use in a TypeScript project.
Independent Models
let table = "my_table_name";
let employees = new Entity(EmployeesModel, { client, table });
let tasks = new Entity(TasksModel, { client, table });
Joining Entity instances to a Service
// Joining Entity instances to a Service
let TaskApp = new Service("TaskApp", { client, table });
TaskApp
.join(employees) // available at TaskApp.entities.employees
.join(tasks); // available at TaskApp.entities.tasks
Joining models to a Service
let TaskApp = new Service("TaskApp", { client, table });
TaskApp
.join(EmployeesModel) // available at TaskApp.entities.employees (based on entity name in model)
.join(TasksModel); // available at TaskApp.entities.tasks (based on entity name in model)
Joining Entities or Models with an alias
let TaskApp = new Service("TaskApp", { client, table });
TaskApp
.join("personnel", EmployeesModel) // available at TaskApp.entities.personnel
.join("directives", TasksModel); // available at TaskApp.entities.directives
Joining Entities at Service construction for TypeScript
let TaskApp = new Service({
personnel: EmployeesModel, // available at TaskApp.entities.personnel
directives: TasksModel, // available at TaskApp.entities.directives
});
When joining a Model/Entity to a Service, ElectroDB will perform a number of validations to ensure that Entity conforms to expectations collectively established by all joined Entities.
- Entity names must be unique across a Service.
- Collection names must be unique across a Service.
- All Collections map to on the same DynamoDB indexes with the same index field names. See Indexes.
- Partition Key [Composite Attributes](#composite attribute-arrays) on a Collection must have the same attribute names and labels (if applicable). See Attribute Definitions.
- The name of the Service in the Model must match the Name defined on the Service instance.
- Joined instances must be type Model or Entity.
- If the attributes of an Entity have overlapping names with other attributes in that service, they must all have compatible or matching attribute definitions.
- All models conform to the same model format. If you created your model prior to ElectroDB version 0.9.19 see section Version 1 Migration.
Model
Create an Entity's schema. In the below example.
const DynamoDB = require("aws-sdk/clients/dynamodb");
const {Entity, Service} = require("electrodb");
const client = new DynamoDB.DocumentClient();
const EmployeesModel = {
model: {
entity: "employees",
version: "1",
service: "taskapp",
},
attributes: {
employee: {
type: "string",
default: () => uuidv4(),
},
firstName: {
type: "string",
required: true,
},
lastName: {
type: "string",
required: true,
},
office: {
type: "string",
required: true,
},
title: {
type: "string",
required: true,
},
team: {
type: ["development", "marketing", "finance", "product", "cool cats and kittens"],
required: true,
},
salary: {
type: "string",
required: true,
},
manager: {
type: "string",
},
dateHired: {
type: "string",
validate: /^\d{4}-\d{2}-\d{2}$/gi
},
birthday: {
type: "string",
validate: /^\d{4}-\d{2}-\d{2}$/gi
},
},
indexes: {
employee: {
pk: {
field: "pk",
composite: ["employee"],
},
sk: {
field: "sk",
composite: [],
},
},
coworkers: {
index: "gsi1pk-gsi1sk-index",
collection: "workplaces",
pk: {
field: "gsi1pk",
composite: ["office"],
},
sk: {
field: "gsi1sk",
composite: ["team", "title", "employee"],
},
},
teams: {
index: "gsi2pk-gsi2sk-index",
pk: {
field: "gsi2pk",
composite: ["team"],
},
sk: {
field: "gsi2sk",
composite: ["title", "salary", "employee"],
},
},
employeeLookup: {
collection: "assignments",
index: "gsi3pk-gsi3sk-index",
pk: {
field: "gsi3pk",
composite: ["employee"],
},
sk: {
field: "gsi3sk",
composite: [],
},
},
roles: {
index: "gsi4pk-gsi4sk-index",
pk: {
field: "gsi4pk",
composite: ["title"],
},
sk: {
field: "gsi4sk",
composite: ["salary", "employee"],
},
},
directReports: {
index: "gsi5pk-gsi5sk-index",
pk: {
field: "gsi5pk",
composite: ["manager"],
},
sk: {
field: "gsi5sk",
composite: ["team", "office", "employee"],
},
},
},
};
const TasksModel = {
model: {
entity: "tasks",
version: "1",
service: "taskapp",
},
attributes: {
task: {
type: "string",
default: () => uuid(),
},
project: {
type: "string",
},
employee: {
type: "string",
},
description: {
type: "string",
},
},
indexes: {
task: {
pk: {
field: "pk",
composite: ["task"],
},
sk: {
field: "sk",
composite: ["project", "employee"],
},
},
project: {
index: "gsi1pk-gsi1sk-index",
pk: {
field: "gsi1pk",
composite: ["project"],
},
sk: {
field: "gsi1sk",
composite: ["employee", "task"],
},
},
assigned: {
collection: "assignments",
index: "gsi3pk-gsi3sk-index",
pk: {
field: "gsi3pk",
composite: ["employee"],
},
sk: {
field: "gsi3sk",
composite: ["project", "task"],
},
},
},
};
Model Properties
Property | Description |
---|---|
model.service | Name of the application using the entity, used to namespace all entities |
model.entity | Name of the entity that the schema represents |
model.version | (optional) The version number of the schema, used to namespace keys |
attributes | An object containing each attribute that makes up the schema |
indexes | An object containing table indexes, including the values for the table's default Partition Key and Sort Key |
Service Options
Optional second parameter
Property | Description |
---|---|
table | The name of the dynamodb table in aws. |
client | (optional) An instance of the docClient from the aws-sdk for use when querying a DynamoDB table. This is optional if you wish to only use the params functionality, but required if you actually need to query against a database. |
Attributes
Attributes define an Entity record. The AttributeName
represents the value your code will use to represent an attribute.
Pro-Tip: Using the
field
property, you can map anAttributeName
to 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.
Simple Syntax
Assign just the type
of the attribute directly to the attribute name. Types currently supported options are "string", "number", "boolean", an array of strings representing a fixed set of possible values, or "any" which disables value type checking on that attribute.
attributes: {
<AttributeName>: "string" | "number" | "boolean" | "list" | "map" | "set" | "any" | string[] | ReadonlyArray<string>
}
Expanded Syntax
Use the expanded syntax build out more robust attribute options.
attributes: {
<AttributeName>: {
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[]
}
}
NOTE: When using get/set in TypeScript, be sure to use the
?:
syntax to denote an optional attribute onset
Attribute Definition
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. |
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. |
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 composition to prefix key composite attributes. By default, the AttributeName is used as the label. |
cast |
"number" , "string" , "boolean" |
no | all | Optionally cast attribute values when interacting with DynamoDB. Current options include: "number", "string", and "boolean". |
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 |
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
.
NOTE: 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"
or "number"
.
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.
NOTE: 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"
}
}
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.
NOTE: 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
orupdate
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.
NOTE: 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
:
NOTE: 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 needs 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 Watching Examples
Example 1 - A calculated attribute that depends on the value of another attribute:
In this example, we have an attribute "fee"
that needs to be updated any time an item's "price"
attribute is updated. The attribute "fee"
uses watch
to have its setter callback called any time "price"
is updated via a put
, create
, update
, or patch
operation.
{
model: {
entity: "products",
service: "estimator",
version: "1"
},
attributes: {
product: {
type: "string"
},
price: {
type: "number",
required: true
},
fee: {
type: "number",
watch: ["price"],
set: (_, {price}) => {
return price * .2;
}
}
},
indexes: {
pricing: {
pk: {
field: "pk",
composite: ["product"]
},
sk: {
field: "sk",
composite: []
}
}
}
}
Example 2 - Making a virtual attribute that never persists to the database:
In this example we have an attribute "displayPrice"
that needs its getter called anytime an item's "price"
attribute is retrieved. The attribute "displayPrice"
uses watch
to return a formatted price string based whenever an item with a "price"
attribute is queried. Additionally, "displayPrice"
always returns undefined
from its setter callback to ensure that it will never write data back to the table.
{
model: {
entity: "services",
service: "costEstimator",
version: "1"
},
attributes: {
service: {
type: "string"
},
price: {
type: "number",
required: true
},
displayPrice: {
type: "string",
watch: ["price"],
get: (_, {price}) => {
return "