Skip to content
This repository has been archived by the owner on Apr 13, 2019. It is now read-only.

schema overview

Konstantin Sobolev edited this page Jan 24, 2018 · 15 revisions

Intro

Epigraph schema describes two things: type system and resources that use [projections] to declare operations. This document focuses on the type system.

Domain model

Almost every system design starts with defining domain models, what do they consist of and how they relate to each other. This is the language product managers and customers understand and it allows to set up universal dictionary for interfaces and implementations. So lets start with a few abstract entities as an example:

Every entity can be described by multiple representations: ID, URI, record or even binary data like images. We can capture this concept by introducing entity types, collections of types that can act as different representations, or models of the same entity.

Entity types

User is a entity type that can be either UserId or UserRecord, File can be either FileId or FileRecord and so on.

Now we have records, fields and field types. FileRecord.name is probably a string, but what’s the type of FileRecord.owner? Before deciding on it lets step back and think about the nature of a field. Fields define relationships between entities and field names hint at their semantics. We don’t care about implementation details at this point, all we want to say is that a File has an owner which is a User.

owner field can hold any representation of a User, and so it's type should naturally be User.

So now we can say that User is a entity type which can be either a UserId or a UserRecord. When clients make requests for this field they have to specify one or more models they want to deal with: either UserId or UserRecord or may be even both.

Entity type is a collection of types that can act as different representations, or models of the same entity type. This is not always an "is-a" relationship, so we can't model it as inheritance, this is more of "can-be-a".

Instances of entity types are internally modelled as records, with field names being type aliases. For example FileRecord.owner of type User can contain

{
  "UserId" : "data"
}

or

{
  "UserRecord" : "data"
}

or both at the same time:

{
  "UserId" : "data",
  "UserRecord" : "data"
}

Versioning

Entity types are a key concept for federator implementation, but they also help to deal with backwards-incompatible changes in the models. Imagine File.owner was erroneously or historically typed as UserId disallowing any models other than UserId. Changing it’s type to UserRecord would be an incompatible change, but making it a User with two representations would work if there was a notion of a default representation. If default representation is set to be a UserId, then legacy clients that don't specify representations explicitly would still work. There exists a special keyword for this, retro, as it is used to retrofit old clients with a new schema without breaking them.

Of course other actions such as removing or renaming a field are still backwards-incompatible and should be avoided.

Semantic types

Imagine that UserId, FileId and EnterpriseId are just long values. It is possible to simply use long type instead, but creating special-purpose types has a number of benefits:

  • Clarity. Meaning of the value follows from it's type
  • Safety. You can add up two long values, but adding UserId to FileId makes no sense
  • Computers can reason about it too. If there is an automatic conversion from UserId to UserRecord then we can try to apply it to a given UserId instance. We couldn't do the same for an arbitrary long value

Such special-purpose types are called sematic types, and usually they are based on one of the primitive types.

Supported types

Framework supports the following type kinds:

  • Entity types such as User which are named collections of aliased types that can act as different representations of the same entity
  • Primitives. Both built-in ones like long or string and user-defined ones like UserId. User-defined primitive types must be based on one of the built-it primitive types
  • Records. As usual, records have named fields, fields have types and can be pivotable. Records can inherit from another records, which means that they get all of the fields from the parents and their instances can be used where parent instances are expected. In other words this is a real 'is-a' relationship. Inherited fields can be overridden to have more specific types.
  • Enums. still being worked on
  • Maps with typed keys and values. Keys can't be pivotable (can't be described by entity types), while values can
  • Lists with typed values

All non-entity (i.e. records, maps, lists, primitives) types are called model types.

Inheritance is only allowed between types of the same kind. Multiple inheritance is supported.

Metadata

Any user-defined model type can have metadata type associated with it. If such association is defined then every instance of this type will have a (possibly empty) metadata instance along with it.

As an example imagine we're modelling Users service and one of the operations returns a list of users. This list can be very big, so we need pagination. Since framework doesn't provide any built-in pagination support out of the box, we will have to invent our own, and metadata is the right mechanism for describing pagination context. Consider the following schema:

list[User] UserList meta OffsetBasedPagingInfo

record OffsetBasedPagingInfo {
  long start,
  long offset
}

Now every instance of UserList will have an optional paging info metadata associated with it containing start and offset attributes.

Clone this wiki locally