#Schema Customization
The schema defines the data structure for your inputs and outputs of your Tilores application. The schema can be fully customized to match your existing data model.
You define the schema by defining how your custom GraphQL API looks like. For that reason, if you already know the basics of how to define a GraphQL schema, then it should be pretty straight forward to define your data model. If you do not have experience with that, we will show you the basics here and additionally would like to point you to the official GraphQL documentation.
By default all files relevant for customizing your schema, can be found in the schema
folder in your Tilores project. Feel free to add your own files as you need them.
#Record
A record represents a single data point of your entity and is the most important data structure to store any kind of information in. It can be very simple, e.g. a single attribute, or very complex, e.g. multiple nested layers of attributes.
You can find the definition for a record in schema/record.graphqls
.
In GraphQL a data structure that can be used in the request as well as in the response can have separate type definitions. That is the reason, why you will find the two types RecordInput
and Record
defined. RecordInput
will be used when submitting data, while Record
will be used when querying data. For most users these will contain the same fields. Later we will show under which circumstances these types might differ.
For now, let's look at the current definition of the RecordInput
.
The input
keyword tells GraphQL that this type is supposed to be used as input and cannot be queried. RecordInput
obviously defines the name of the type as it must be used in other places. And our RecordInput
currently has two fields, id
and myCustomField
.
The ID field is the only field that is required by Tilores. It must be unique among all records and we highly recommend to use random UUIDs (V4) for this purpose. If this would break your data model, there is the possibility to rename or generate the ID field.
myCustomField
only serves as a simple example and can easily be replaced with your own fields. However, be aware, that the default configuration uses this field for matching and searching and the rules configuration should be modified when removing this field.
Each field can have a multiline description. This description can be queried and displayed by GraphQL clients and serves as the documentation for your custom API - a GraphQL feature called introspection.
#Adding Fields
Let's assume, you have JSON data and it looks something like this:
Adjusting the RecordInput
to have the sourceID
available is a simple task:
You could proceed the same way with the source. However, in that case you may already know that there is a limited amount of possible sources and you may want to model it as an enumeration.
When running or deploying your project, the relevant enumeration type will be created for you.
For the person
field we will have to create a new type:
Notice, that the address
definition does not end with an exclamation mark !
. This is meant to show how to define optional fields. In that case you can either omit the address or submit it using a null
value.
Both, the source
enumeration and the PersonInput
can either be defined directly inside the schema/record.graphqls
file or in new files. This is completely up to you.
The final RecordInput
could look something like this:
Now that the RecordInput
is defined, it is an easy task to apply the changes to the Record
:
Notice, that also the PersonInput
must be copied!
#SearchParams
The type SearchParams
lets you define which fields can be used during a search query. You can find its definition in schema/search.graphqls.
.
The default type definition is again just for demonstration purposes and looks like this:
Again it is up to you to decide how a search is supposed to look like. Using the previously defined model for the record, you might e.g. provide the possibility to search using the name and the address. In that case you could alter the SearchParams
to either use the existing PersonInput
type, define a new type PersonSearchInput
or just provide the relevant fields directly in the SearchParams
.
Option 1: reuse existing type:
Option 2: define a new type:
Option 3: provide only relevant fields:
Note, that the field names do not have to be equal to the field names as they were defined in the PersonInput
. In most cases option two or three are the preferable, because they can evolve independently from the RecordInput
, e.g. the PersonInput
might introduce a new field dateOfBirth
which is not relevant for the search.
Also note, that in order to use the fields defined in the SearchParams
they must be used in the search rules configuration.
#Default Scalars
GraphQL comes with a list of default types, so called scalars. Additionally a few more scalars are available.
The default scalars can be found in the official documentation.
The following additional scalars are defined by default.
In some cases you may want to define your own scalar types.
#Advanced Configuration
The following examples are more advanced, because they require small changes or additions in the generated source code.
#Diverging Record and RecordInput
Defining the structure of Record
and RecordInput
independently seems very annoying at first. However, it provides great possibilities to evolve the underlying data model from the query model. Maybe you would like to return certain available data only in an aggregated form or you would like to enrich data that is stored in another system. There are probably limitless amounts of reasons, why you might need this.
#Data Enrichment
Let's assume we have an InputRecord
that, beside other data, has a providerID
field. That data about the provider is stored in an external database. When querying we want that the user can query the providers name and contract id. Since these values are subject of change and not relevant for our matching process, we do not want to store them inside Tilores.
Our schema might look like this:
When running this schema, the provider data would be empty. More precisely, it would even fail with an error, because the required provider
field will always be empty.
To fix this, first we need to create a custom model, that has the providerID
field. Create the file graph/model/record.go
:
Now tell the schema to use that type instead of generating one:
Regenerate the models and resolvers by running go generate ./...
on your project folder.
Since our record does not define a provider
field, but our schema needs an implementation for one, the new file graph/record.resolvers.go
was created for you and should contain something like this:
All that is left is to actually implement this resolver method. In a real scenario you would most likely make a database query or a call to another service. In our example, we are going to create a fake implementation.
#Requiring New Field With Existing Data
When adding a new required field to the RecordInput
after you have already submitted data into your Tilores instance, then that same field should not be required on the Record
. If you ignore this rule, then this will result in errors when trying to query that new field on records that do not have this field and therefore would return a null
value.
If you have to mark that field as required on the Record
, then the alternative would be to enforce the creation of a resolver for that field and return an appropriate default value if the value would otherwise be null
.
Assuming your schema looks like this:
Furthermore assuming you already have data added according to that schema. Now you want to add another required field to the RecordInput
:
For the Record
the simplest solution would be to add newField: String
(without an exclamation mark). This would allow newField
, when queried to return null
.
For cases when you must ensure that this value is not nullable, then you can define the field like this:
Additionally, you need to create or modify your Record
model under graph/model/record.go
like this:
After you regenerated the code using go generate ./...
, you should find the following function under graph/record.resolvers.go
:
It should now be trivial to implement a default value for that field (value is missing
in our example):
#Custom Record IDs
As mentioned before, each record must have an ID. Depending on your needs, you may not want to stick with the default implementation. The following three scenarios show you how to adjust it for your use case.
A record ID must never contain a colon :
! Colons are used to separate the IDs and labels of edges. A record with a colon in the ID will be rejected with an error message.
#Scenario 1: Rename ID Field
If your data already contains a unique ID field, but you want to stick to your existing name, you can easily change that.
First you need to change the field name in schema/record.graphqls
for both the Record
and RecordInput
:
We replaced id
with customID
.
Then you have to change the code that extracts the ID field. In graph/helpers.go
you will find the following function:
Change that to the correct field name:
After regenerating the API code using go generate ./...
, the compile errors will be gone and your field will be renamed.
#Scenario 2: Merged ID
Another scenario might be that you have unique ID, but only within each data provider. The steps are similar to the first scenario.
Change the record schema:
Change the extractRecordID
function to:
#Scenario 3: Generated Random IDs
If you have no fields available that can serve as the record ID, your only possibility left is to generate a random ID. The steps are similar to the first scenario.
Remove the ID field from the record schema:
Change the extractRecordID
function to: