Schema
A Schema
is a list of attributes that describe the items of an Entity
:
import { schema } from 'dynamodb-toolbox/schema'
import { string } from 'dynamodb-toolbox/attributes/string'
import { number } from 'dynamodb-toolbox/attributes/number'
const pokemonSchema = schema({
pokemonId: string().key(),
level: number().default(1),
pokeType: string()
.enum('fire', 'water', 'grass')
.optional()
})
const PokemonEntity = new Entity({
...,
schema: pokemonSchema
})
Schemas always start with a root object, listing attributes by their names.
Attribute Types
Schema attributes can be imported by their dedicated exports, or through the attribute
or attr
shorthands. For instance, those declarations output the same attribute:
// 👇 More tree-shakable
import { string } from 'dynamodb-toolbox/attributes/string'
const nameAttr = string()
// 👇 Less tree-shakable, but single import
import {
attribute,
attr
} from 'dynamodb-toolbox/attributes'
const nameAttr = attribute.string()
const nameAttr = attr.string()
Available attribute types are:
any
- Contains any valuenull
- Contains nullboolean
- Contains booleansnumber
: Contains numbersstring
: Contains stringsbinary
: Contains binariesset
: Contains sets of eithernumber
,string
, orbinary
elementslist
: Contains lists of elements of any typemap
: Contains maps, i.e. a finite list of key-value pairs, values being child attributes of any typerecord
: Contains a different kind of maps - Records differ frommaps
as they have a non-explicit (potentially infinite) range of keys, but with a single value typeanyOf
: Contains a finite union of possible attributes
DynamoDB-Toolbox attribute types closely mirror the capabilities of DynamoDB. See the DynamoDB documentation for more details.
Note that some attribute types can be defined with other attributes. For instance, here's a list of string:
const nameAttr = string()
const namesAttr = list(nameAttr)
Fine-Tuning Attributes
You can update attribute properties by using dedicated methods or by providing option objects.
The former provides a slick devX with autocomplete and shorthands, while the latter theoretically requires less compute time and memory usage (although it should be negligible):
// Using methods
const pokemonName = string().required('always')
// Using options
const pokemonName = string({ required: 'always' })
Attribute methods do not mute the origin attribute, but return a new attribute (hence the impact in memory usage).
The output of an attribute method is also an attribute, so you can chain methods:
const pokeTypeAttr = string()
.required('always')
.enum('fire', 'water', 'grass')
.savedAs('t')
Warm vs Frozen
Prior to being wrapped in a schema
declaration, attributes are called warm: They are not validated (at run-time) and can be used to build other schemas. By inspecting their types, you can see that they are prefixed with $
.
const $nameSchema = string().required('always')
// => $StringAttribute
Once frozen, validation is applied and building methods are stripped:
const nameSchema = $nameSchema.freeze()
// => StringAttribute
nameSchema.required
// => 'always'
nameSchema.required('never')
// => ❌ 'required' is not a function
Wrapping attributes in a schema
declaration freezes them under the hood:
const pokemonSchema = schema({ name: $nameSchema })
// => Schema<{ name: StringAttribute }>
pokemonSchema.attributes.name.required
// => 'always'
The main takeaway is that warm schemas can be composed while frozen schemas cannot:
const pokemonSchema = schema({
// 👍 No problemo
pokemonName: $nameSchema,
...
});
const pokedexSchema = schema({
// ❌ Not possible
pokemon: pokemonSchema,
...
});
Updating Schemas
As we've just seen, once frozen, schemas cannot be updated.
However, you can use them to build new schemas with the following methods:
and(...)
(attr: $NEW_ATTR | (Schema<OLD_ATTR> => $NEW_ATTR)) => Schema<OLD_ATTR & NEW_ATTR>
Allows extending a schema with new attributes:
const extendedSchema = baseSchema.and({
newAttribute: string(),
...
})
In case of naming conflicts, new attributes override the previous ones.
The method also accepts functions that return a (warm) schema. In this case, the previous schema is provided as an argument (which is particularly useful for building Links):
const extendedSchema = mySchema.and(prevSchema => ({
newAttribute: string(),
...
}))
pick(...)
(...attrNames: ATTR_NAMES[]) => Schema<Pick<ATTR, ATTR_NAMES>>
Produces a new schema by keeping only certain attributes of the original schema:
const picked = pokemonSchema.pick('name', 'pokemonLevel')
Due to the potential disruptive nature of this method on links, they are reset in the process:
const nameSchema = schema({
firstName: string(),
lastName: string(),
completeName: string().link(({ firstName, lastName }) =>
[firstName, lastName].join(' ')
)
})
const picked = nameSchema.pick('lastName', 'completeName')
picked.attributes.completeName.links.put
// => undefined
omit(...)
(...attrNames: ATTR_NAMES[]) => Schema<Omit<ATTR, ATTR_NAMES>>
Produces a new schema by removing certain attributes out of the original schema:
const omitted = pokemonSchema.omit('name', 'pokemonLevel')
Due to the potential disruptive nature of this method on links, they are reset in the process:
const nameSchema = schema({
firstName: string(),
lastName: string(),
completeName: string().link(({ firstName, lastName }) =>
[firstName, lastName].join(' ')
)
})
const omitted = nameSchema.omit('firstName')
omitted.attributes.completeName.links.put
// => undefined