-
Notifications
You must be signed in to change notification settings - Fork 4
transformers
Transformers are the foundation of federation, they define data transformations that can be applied by the federator
to some initial data in order to achieve desired result. For example there can be transformers for resolving
PersonRecord
from PersonId
, or for building fullName
field from firstName
and lastName
.
Every transformer is defined for some type T
and is described by two projections: input and output. Transformer
implementation takes data of type T
conforming to input projection and produces another instance of T
conforming
to the output projection.
Transfomers can be defined in Epigraph schema using transformer
keyword and the following syntax:
transformer <name> : <T> {
<optional annotations>
inputProjection <input projection of type T>
outputProjection <output projection of type T>
}
Lets consider the following types
entity Person {
id: PersonId
`record`: PersonRecord
}
long PersonId
record PersonRecord {
firstName: String
lastName: String
fullName: String
bestFriend: Person
}
Transformer that converts PersonId
to PersonRecord
could look like this:
transformer person: Person {
inputProjection :+id // '+' makes it 'required' meaning it can't be an error or null
outputProjection :`record`(firstName, lastName)
}
This transformer applies to Person
entity type and can convert it's id
model to record
model, with firstName
and
lastName
fields populated.
Another transformer could be responsible for best friend lookup:
transformer bestFriend: Person {
inputProjection :+id
outputProjection :`record`(bestFriend:id)
}
This says that given person's ID this transformer can find best friend's ID. Previously described 'person'
transformer could then be applied to build best friend's record
model.
Yet another transformer can be used to compose full names out of first and last names:
transformer fullName: PersonRecord {
inputProjection (firstName, lastName)
outputProjection (fullName)
}
Read operations on map
-typed resources can be treated as keys to values conversions. Transformers can
be automatically derived from such operations as soon as we know how keys and values are related.
For example consider people
resource:
resource people: map[PersonId,Person] {
read {
outputProjection [required]:`record`(firstName,lastName)
}
}
Here we have a single read operation that takes (required) map keys as an argument and produces record models. This
looks very close to the person
transformer described in the examples section above. We can derive it
as soon as we know that PersonId
key and Person
map value are the same entity, like this:
resource people: map[PersonId,Person] {
read {
outputProjection [required]:`record`(firstName,lastName)
transformer: Person {
inputPath :+id
}
}
}
This says that
- we are defining a transformer of
Person
type - it takes
id
models as an input and translates them to map keys - operation output projection without key part becomes transformer output projection,
resulting in
:record(firstName, lastName)
Transformer's inputPath
defines a path from transofmer type Person
to map key type. outputPath
similarly defined a path from Person
to map value type, which is empty in our case.
Generated trasformer implementation will be calling read operation with proper parameters translation, employing calls batching when possible.
What if people
resource was defined as a map from PersonId
to PersonRecord
? Read operation's output projection
wouldn't work out of the box anymore:
resource people: map[PersonId,PersonRecord] {
read {
outputProjection [required](firstName,lastName)
transformer: Person {
inputPath :+id
outputPath :`record`
}
}
}
Now we have to explictly tell that map values are record
models of Person
type. Operation's output projection
will be appended to the transformer's :record
output path, with key part removed, giving the same result
as before.
Best friend operation can be modelled in two different ways. First one would be the same people
resource, but a
separate read operation with a path:
resource people: map[PersonId,Person] {
read bestFriend {
path /.:record/bestFriend
outputProjection :id
transformer: Person {
inputPath :+id
}
}
}
In this case operation defines a path relative to map value type that it can be applied to. For instance this operation will be called for
GET /people/1:record/bestFriend:id
request, but not for
GET /people/1:record(firstName,bestFriend:id)
Transformer's output path will consist of operation path with key part removed, followed by operation's output
projection, resulting in :record/bestFriend:id
Another way to define best friend operation is
resource bestFriends: map[PersonId,PersonId] {
read {
outputProjection [required]
transformer: Person {
inputPath :id
outputPath :record/bestFriend:id
}
}
}
This operation defines a straightforward mapping, and in this case transformer must be fully explicit about the relationship between map key and value.
Given the following schema
resource RES_NAME: map[K,V] {
read OP_NAME {
path OP_PATH
outputProjection [ projection: OP_KEY ] OP_VALUE
transformer T_TYPE {
inputPath T_INPUT_PREFIX
outputPath T_OUTPUT_PREFIX
}
}
}
where
-
OP_PATH
,OP_KEY
andOP_VALUE
are optional (by resource syntax) -
T_INPUT_PREFIX
is optional ifK
isT_TYPE
-
T_INPUT_PREFIX
is a path starting with typeT_TYPE
and ending withK
type -
T_OUTPUT_PREFIX
is optional ifV
isT_TYPE
-
T_OUTPUT_PREFIX
is a path starting with typeT_TYPE
and ending withV
type - Resource map keys may be
required
, but may not beforbidden
a transformer can be derived:
transformer T_NAME: T_TYPE {
inputProjection T_INPUT
outputProjection T_OUTPUT
}
where
-
T_NAME
is${RES_NAME}_${OP_NAME or "read" if no name}_transformer
-
T_INPUT
isT_INPUT_PREFIX
+OP_KEY
-
T_OUTPUT
isT_OUTPUT_PREFIX
+noKey(OP_PATH)
+OP_VALUE2
-
noKey
removes leading key part from projection (essentially returns map projection's value part) -
OP_VALUE2
isOP_VALUE
ifOP_PATH
is present,noKey(OP_VALUE)
otherwise
-