Skip to main content

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 value
  • null - Contains null
  • boolean - Contains booleans
  • number: Contains numbers
  • string: Contains strings
  • binary: Contains binaries
  • set: Contains sets of either number, string, or binary elements
  • list: Contains lists of elements of any type
  • map: Contains maps, i.e. a finite list of key-value pairs, values being child attributes of any type
  • record: Contains a different kind of maps - Records differ from maps as they have a non-explicit (potentially infinite) range of keys, but with a single value type
  • anyOf: Contains a finite union of possible attributes
info

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)
info

Schemas are a standalone feature of DynamoDB-Toolbox (you can use them separately to parse and format data for instance) and might even be moved into a separate library one day.

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' })
info

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(),
...
})
info

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