-
Notifications
You must be signed in to change notification settings - Fork 4
schema overview
Epigraph schema describes two things: type system and resources that use [projections] to declare operations. This document focuses on the type system.
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.
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"
}
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.
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 addingUserId
toFileId
makes no sense - Computers can reason about it too. If there is an automatic conversion from
UserId
toUserRecord
then we can try to apply it to a givenUserId
instance. We couldn't do the same for an arbitrarylong
value
Such special-purpose types are called sematic types, and usually they are based on one of the primitive 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
orstring
and user-defined ones likeUserId
. 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.
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.