Skip to the content.

Schema

This component uses a composer to filter and validate a data structure in accordence to a defiend schema.


Availible validators and filters

In below table you can see which type of validators and filters that are availible to each schema type that is titled in the column.

  csv boolean decimal email integer json schema string timestamp
default
collection boolean boolean boolean boolean boolean boolean boolean boolean boolean
collection-size-min number number number number number number number number number
collection-size-max number number number number number number number number number
optional boolean boolean boolean boolean boolean boolean boolean boolean boolean
nullable boolean boolean boolean boolean boolean boolean boolean boolean boolean
unsigned boolean boolean boolean
min number number number number timestamp
max number number number number timestamp
gt number number number number timestamp
lt number number number number timestamp
length number
enum array array array array array array
uppercase boolean boolean boolean
lowercase boolean boolean boolean
not-empty boolean boolean boolean
decode boolean boolean
stringified boolean
indentation number
schema string
trait string
date boolean
local boolean
localDate boolean
localTime boolean
utc boolean
json boolean

Example application

This example will show a simple application with an expected flow that uses schemas at route level, as well as in context-mapper, the infrastructural anti-corruption layer.

app
├── src
│   ├── api
│   │   ├── endpoint
│   │   │   └── fetch-person.js
│   │   └── config.js
│   ├── domain
│   │   ├── aggregate
│   │   │   └── person
│   │   │       ├── index.js
│   │   │       └── locator.js
│   │   ├── schema
│   │   │   ├── dto
│   │   │   │   └── query-person.js
│   │   │   └── entity
│   │   │       └── person.js
│   │   └── config.js
│   ├── infrastructure
│   │   ├── db
│   │   │   └── repository
│   │   │       ├── index.js
│   │   │       ├── locator.js
│   │   │       └── mapper.js
│   │   └── config.js
│   └── index.js
└── package.json

Above is an overview of a defined filestructure of an application that has only 1 endpoint, that is to fetch a person.


Api

The api layer is where the interactable interface to the application is defined.


app/src/api/endpoint/fetch-person.js

const Dispatcher = require('superhero/core/http/server/dispatcher')

class EndpointPerson extends Dispatcher
{
  async dispatch()
  {
    const
    aggregate = this.locator.locate('aggregate/person'),
    person    = await person.fetch(this.route.dto)

    this.view.body = person
  }
}

module.exports = EndpointPerson

The endpoint above locates the aggregate for the person through wich a person is querried with the help of the dto, Data Transfer Object, mapped by the route. The entity “person” is set to the view to be returned in the api call.


app/src/api/config.js

module.exports =
{
  core:
  {
    http:
    {
      routes:
      {
        'fetch-person':
        {
          url       : '/',
          method    : 'get',
          endpoint  : 'api/endpoint/fetch-person',
          view      : 'core/http/server/view/json',
          input     : 'dto/query-person',
          output    : 'entity/person'
        }
      }
    }
  }
}

In the configuration of the api layer, we define the route with the input and output schemas refferenced.


Domain

The domain layer is where logic related to the context of the application is located.


app/src/domain/aggregate/person/index.js

class AggregatePerson
{
  constructor(repository)
  {
    this.repository = repository
  }

  fetch(dto)
  {
    return this.repository.fetchPerson(dto)
  }
}

module.exports = AggregatePerson

An aggregate is defined in the domain layer that is dependent on a repository that can fetch a person. In DDD, Domain Driven Design, this dependency is expected to be contract oriented.


app/src/domain/aggregate/person/locator.js

const
AggregatePerson     = require('.'),
LocatorConstituent  = require('superhero/core/locator/constituent')

class AggregatePersonLocator extends LocatorConstituent
{
  locate()
  {
    const
    repository = this.locator.locate('db/repository'),
    aggregate  = new AggregatePerson(repository)

    return aggregate
  }
}

module.exports = AggregatePersonLocator

In the locator constituent for the “person” aggregate, the db repository is located and injected into the aggregate on construction.


Domain / Schema

In a sub layer to the domain we expect to find the schemas that defines the domains data structures.

The schema layer has different sub layers as well; dto, entity and value-object are some common layers used by the DDD community in this scope.


app/src/domain/schema/dto/query-person.js

module.exports =
{
  'id':
  {
    'type'      : 'integer',
    'unsigned'  : true
  }
}

Above is a simple dto schema that defines a query used to fetch a person. As the query only has an attribute named id defined, a person can only be fetched by id in this example.


app/src/domain/schema/entity/person.js

module.exports =
{
  'id':
  {
    'type'      : 'integer',
    'unsigned'  : true
  },
  'name':
  {
    'type'      : 'string',
    'not-empty' : true
  },
  'age':
  {
    'type'      : 'integer',
    'unsigned'  : true
  }
}

In the entity layer we find the schema for the person defined. The entity has 3 attributes declared; id, name and age. The schema defines validation and filtration rules that are related to the data structure.


app/src/domain/config.js

module.exports =
{
  core:
  {
    locator:
    {
      'aggregate/person'    : __dirname + '/aggregate/person'
    },
    schema:
    {
      composer:
      {
        'dto/query-person'  : __dirname + '/schema/dto/query-person',
        'entity/person'     : __dirname + '/schema/entity/person'
      }
    }
  }
}

In the configuration file of the domain layer references are decalared to the agregate and the the schemas as shown above.


Infrastructure

The infrastructure layer is the layer that contains logic related to interaction with external services, such as the database.


app/src/infrastructure/db/gateway/locator.js

const
mysql               = require('mysql'),
Db                  = require('@superhero/db'),
AdapterFactory      = require('@superhero/db/adapter/mysql/factory'),
LocatorConstituent  = require('superhero/core/locator/constituent')

class DbGatewayLocator extends LocatorConstituent
{
  locate()
  {
    const
    configuration   = this.locator.locate('core/configuration'),
    adapterFactory  = new AdapterFactory(),
    options         = configuration.find('infrastructure/db/gateway'),
    filePath        = __dirname + '/../sql',
    fileSuffix      = '.sql',
    adapter         = adapterFactory.create(mysql, options),
    gateway         = new Db(adapter, filePath, fileSuffix),

    return gateway
  }
}

module.exports = DbGatewayLocator

In this example we use the external component @superhero/db that is published on npm as the gateway. See in-dept documentation at the public repository for more information.


app/src/infrastructure/db/repository/index.js

const
DbRepository        = require('.'),
LocatorConstituent  = require('superhero/core/locator/constituent')

class DbRepository
{
  constructor(gateway, mapper)
  {
    this.gateway  = gateway
    this.mapper   = mapper
  }

  async fetchPerson(dto)
  {
    const
    result  = await this.gateway.query('person/fetch', [dto.id]),
    person  = this.mapper.mapPerson(result)

    return person
  }
}

module.exports = DbRepository

As seen in the example above, we have a dependency to the db gateway and a mapper, the context-mapper. The mapper will take the result from the database and map the result to an understandable data structure in accordence to the schema defined in the domain.


app/src/infrastructure/db/repository/locator.js

const
DbRepository        = require('.'),
DbRepositoryMapper  = require('./mapper'),
LocatorConstituent  = require('superhero/core/locator/constituent')

class DbRepositoryLocator extends LocatorConstituent
{
  locate()
  {
    const
    gateway     = this.locator.locate('db/gateway'),
    composer    = this.locator.locate('core/schema/composer'),
    mapper      = new DbRepositoryMapper(composer),
    repository  = new DbRepository(gateway, mapper)

    return repository
  }
}

module.exports = DbRepositoryLocator

In the locator for the repository we construct the repository, but we also construct the mapper. The mapper is injected with the located component core/schema/composer.


app/src/infrastructure/db/repository/mapper.js

class DbRepositoryMapper
{
  constructor(composer)
  {
    this.composer = composer
  }

  mapPerson(result)
  {
    if(result.length < 1)
    {
      throw new Error('No results found')
    }

    if(result.length > 1)
    {
      throw new Error('Conflicting results found')
    }

    return this.composer.compose('entity/person', result[0])
  }
}

module.exports = DbRepositoryMapper

In the context-mapper we use the schema composer to validate and filter the result before we return the expected data structure. It is considered good practice to use a composition pattern for the mapper. In this example the mapper is very simple, the mapper only has one responsibility as it is, why I decided to not use a composition pattern in this example.


app/src/infrastructure/db/config.js

module.exports =
{
  core:
  {
    locator:
    {
      'db/gateway'    : __dirname + '/db/gateway',
      'db/repository' : __dirname + '/db/repository'
    }
  },
  infrastructure:
  {
    db:
    {
      gateway:
      {
        connections : 5,
        host        : '...',
        user        : '...',
        password    : '...',
      }
    }
  }
}

In the configuration file of the infrastructure we declare the refferences to the location of the services db/gateway and db/repository. We also define the configuration options to the gateway. The values for host, user and password as 3 dots is placeholders for the values needed in the connection at hand.


app/src/index.js

const
CoreFactory = require('superhero/core/factory'),
coreFactory = new CoreFactory,
core        = coreFactory.create()

core.add('api')
core.add('domain')
core.add('infrastructure')

core.load()

core.locate('core/bootstrap').bootstrap().then(() =>
core.locate('core/http/server').listen(80))

Finally we define the main script that adds and loads the api, domain and the infrastructure components. After bootstrap, the server is instructed to listen to port 80 for incoming api calls.


app/src/package.json

{
  "name": "App",
  "version": "0.0.1",
  "description": "An example app",
  "repository": "https://github.com/...",
  "license": "MIT",
  "main": "src/index.js",
  "dependencies": {
    "superhero": "*",
    "@superhero/db": "*",
    "mysql": "*"
  }
}

Below is a simple example of the package.json file used in this example application.


Meta definition

It is possible to define a meta attribute to your schema, which in turn allows 2 different possible meta descriptions; extends and immutable.


Extends

From above example, we defined a person in a schema. If we want to have an extended type of a person, a superhero which has all the attributes of a person and an additional attribute; superpower, then we can use the meta attribute to extend the person as the example below describes.

module.exports =
{
  '@meta':
  {
    'extends' : 'entity/person'
  },
  'superpower':
  {
    'type'    : 'string',
    'enum'    : ['invisibility', 'precognition', 'indestructible']
  }
}

It is also possible to extend from multiple references by using an array of refferences in the extends attribute.


Excludes

When using extensions, sometimes it is decireable to exclude some attributes of the extended schema. It is possible to exclude attributes from a schema by defining the excludes sub-attribute in the @meta root-attribute.

module.exports =
{
  '@meta':
  {
    'extends'   : 'entity/person',
    'excludes'  : 'age'
  },
  'superpower':
  {
    'type'    : 'string',
    'enum'    : ['invisibility', 'precognition', 'indestructible']
  }
}

It is also possible to exclude multiple attributes by specifying an array in the excludes attribute.


Immutable

By default, the schema will construct an immutable object, an object that can not be changed, a frozen object. If you like a composed structure to instead be mutable, then you must specify that in the meta attribute, as in the example below.

module.exports =
{
  '@meta':
  {
    'immutable' : false
  },
  // ...
}

It is also possible to control the immutable output of the compose method on the schema composer object (core/schema/composer) by passing a third optional argument to the method, as the below example shows.

const 
  immutable = this.composer.compose('schema-name', dto, true),
  mutable   = this.composer.compose('schema-name', dto, false)

Dynamic configuration specification

To make it easier to load schemas, it is possible to use a dynamic reference in the configurations. The example below shows a directory with many different shemas.

app
└── src
    └── domain
        ├── schema
        │   ├── foo.js
        │   ├── bar.js
        │   ├── baz.js
        │   └── qux.js
        └── config.js

To include all these services, the configuration in the domain is expected to look something like the example below shows.

module.exports =
{
  core:
  {
    schema:
    {
      composer:
      {
        'schema/foo' : __dirname + '/schema/foo',
        'schema/bar' : __dirname + '/schema/bar',
        'schema/baz' : __dirname + '/schema/baz',
        'schema/qux' : __dirname + '/schema/qux'
      }
    }
  }
}

By using an asterix * in the configuration defintion, to specify a dynamic reference, the same configuration could look like the following example shows.

module.exports =
{
  core:
  {
    schema:
    {
      composer:
      {
        'schema/*' : __dirname + '/schema/*'
      }
    }
  }
}

Both examples above has the same effect.