The data modeling language extension for GraphQL
This article describes the current capabilities of the Data modeling language extension for GraphQL (DML).
The DML extension lets you control both the data model-centric, and the view-centric —the solution data model—management of a Data Modeling Service (DMS) based schema.
The data modeling REST API (DMS) in combination with the Cognite Python SDK offers more power and flexibility than the DML extension. They're the preferred tools for managing industrial knowledge graphs in Cognite Data Fusion (CDF).
Basics
The data model format is in the GraphQL Schema Definition Language. Each data model has many types, consisting of properties, each with a specified type.
Each type can contain properties that reference other types, forming a relationship. You can also specify the properties as a list of references to nodes of other types.
Use !
to indicate that a field is required and that each instance of the template must contain the value.
To add comments, enter "comment"
above the field/type definition.
This example is a simple industry data model:
type Equipment {
"Unique identifier for the equipment"
id: int!
"name of the piece of equipment"
name: String!
"the parts included to build this equipment"
parts: [Part]
"the documents describing use and maintenance for the equipment"
documentation: [Document] @reverseDirectRelation(throughProperty: "equipment")
}
type Part {
id: int!
name: String!
documentation: [Document]
}
type Document {
id: int!
name: String!
source: String!
"the equipment this document describes"
equipment: Equipment
"the parts this document describes"
parts: [Part]
}
Where:
Equipment
type has the propertiesid
, andname
properties.-
id
must be anInteger
type, and must exist on all nodes ofEquipment
.name
must be aString
type, and must exist on all nodes ofEquipment
.
Equipment
type has aparts
relationship to a list/array of one or morePart
s.Equipment
type has adocumentation
relationship to a list/array of one or moreDocument
s. This is a reverse direct relation, meaning the direct relation property on theDocument
view is used for graph traversal.Part
has the propertiesid
andname
,id
must be anInteger
type, and must exist on all nodes ofPart
.name
must be aString
type, and must exist on all nodes ofPart
.
Part
has adocumentation
relationship to a list/array of one or moreDocument
s.Document
type has the propertiesid
,name
, andFile
.id
must be anInteger
type, and must exist on all nodes ofDocument
.name
must be aString
type, and must exist on all nodes ofDocument
.source
must be aString
type, and must exist on all nodes ofDocument
.
Document
has aequipment
relationship to a single (piece of)Equipment
.Equipment.name
has a comment/description (name of the piece of equipment).
Direct relations can also be lists. To model this, add the @directRelation
directive to the field.
Types and Interfaces
An object type in GraphQL is defined by using the keyword type, for example type Equipment
defines an object type in GraphQL
called Equipment
.
An interface type in GraphQL is defined by using the keyword interface, for example interface Equipment
.
From a data querying or population perspective, there is no difference between a type being an object or interface. However, only interface can be extended / implemented upon.
EquipmentInterface
and EquipmentObject
will here have the same query capabilities:
interface EquipmentInterface {
name: String
}
type EquipmentObject {
name: String
}
Enums
DML also supports GraphQL enum types, which can be defined using the keyword enum
:
enum Status {
ACTIVE
INACTIVE
}
type Equipment {
name: String!
status: Status
}
In our DML, an object or interface type represents a view by default.
When a @view
directive isn't specified, the view version will be managed by the GraphQL service. When a new
view version is needed due to breaking changes, the data model version will also have to be changed.
View versions can also be overriden by directives.
We call types having a '@view' directive for view types. A view type can extend from other view types, allowing for graphql inheritance, more in the next section.
Extending types via inheritance
We rely on the inheritance rules as defined by GraphQL, so to extend another view type, that base view type has to be an interface.
A view type can be extendable via inheritance by defining the type to be an interface. A view type can then extend the base type by
using implements
. You can also extend multiple interfaces by separating them with &
.
This is an example of a view extending another view:
interface Common {
name: String
}
type Equipment implements Common {
name: String
}
There are some caveats with fields that are imported from inheritance, learn more in the advanced section.
Descriptions
GraphQL descriptions are supported and will be converted to the corresponding description property on view, container and
property definitions.
Furthermore, we have syntax for setting the name
of a view / container or a field
. It is set by starting a line in the description
with @name
.
The rest of the line will then be parsed as the name
.
Example of GraphQL descriptions:
"""
This is a common interface for common models
@name Common View
"""
interface Common {
name: String
}
type Equipment implements Common {
"""
This is the description for the property Equipment.name
@name Name
"""
name: String
}
Versioning
Every type is versioned, and the version changes when the view in the data model is updated with a breaking change. As of now, all changes to the structure of a type are considered breaking changes, but if you leave the type unchanged it will not get a new version. Since you can query each version of a data model individually, consumers are not directly affected by breaking changes.
Relations
We support a number of different types of relations - they can point to a single target instance or multiple target instances, and they can be backed by either edges or direct relations.
A relation with a single target instance, for example document: Document
, is backed by a direct relation. A direct relation is a container property storing a reference to another node. These can be used to describe both one to one and one to many relations - for example, an equipment can point to a single document describing it, but multiple equipments may point to the same document.
If you want to define a relation pointing to more than a single instance, you can create a field with a list data type, for example documents: [Document]
. These can be used to describe both one to many and many to many relations - for example, an equipment can point to multiple documents describing it and multiple equipments can point to the same documents.
These relations are by default backed by edges, and they need to be ingested as separate instances. It is possible to specify that relation should be backed by a list of direct relations instead by using the @directRelation
directive.
documents: [Document]
will generate a view connection property that we call a relation in graphql. A relation is defined by the:
- relational direction (edge direction), which by default is OUTWARDS
- the edge.type to follow. The default is set to
{your_typename}.{your_fieldname}
. - the space is by default set to be the same as the view that the field resides in.
However, just as type's name is unique per space, so is the edge.type.
If you want to describe a many to many relation, you simply need to add in a reference in the opposite direction, and use @relation to specify that this relation goes inwards instead of the default outwards. For example,
type Equipment implements Common @view(space: "core") {
name: String
documents: [Document] # This is identical to setting @relation(type: { space: "core", externalId: "Equipment.documents" }, direction: OUTWARDS)
}
interface Document {
name: String
equipments: [Equipment]
@relation(
type: { space: "core", externalId: "Equipment.documents" }
direction: INWARDS
)
}
Categories of types
The following type
s are available:
- Primitives - simple types like
String
,Int
,Int64
,Float
,Boolean
,JSONObject
,TimeSeries
, andTimestamp
. - User defined - any other type that is defined in the data model. In the example above,
Actor
andMovie
are examples of user defined types, forming relationships between types.
All of these types can exist in a list (for example [String]
) or in singular form (for example String
) through [...]
.
Primitive data types
Primitive data types are mapped as following:
GraphQL name | API name | Description |
---|---|---|
String | text | |
Int or Int32 | int32 | 32-bit integer number |
Int64 | int64 | 64-bit integer number |
Float or Float64 | float64 | 64-bit number encoded in IEEE 754 |
Float32 | float32 | 32-bit number encoded in IEEE 754 |
Timestamp | timestamp | Timestamp encoded in ISO 8601 |
JSONObject | json | JSON encoded data |
Date | date | Date encoded in ISO 8601, but without time |
Boolean | boolean |
CDF native resource types
Primitive data types are mapped as following:
GraphQL name | API name | Description | Supported |
---|---|---|---|
File | ✅ | ||
TimeSeries | ✅ | ||
Sequence | ✅ |
1 Beta
We don't support the union GraphQL types, as well as defining input
and extends
despite the feature being available in GraphQL.
How GraphQL SDL concepts are mapped to data models API
The main goal of the DML is to re-use concepts from the GraphQL schema definition language to the extent possible. However, the GraphQL SDL is designed to describe a schema, while we need to translate it into an implementation. There are multiple implementations conforming to the same GraphQL schema. For convenience, the translation from a GraphQL schema to a set of industrial knowledge graph schema components - containers and views - is based on sane defaults. However, this behavior can be overridden as needed by using directives.
Specification of directives
The following is a list of the directive definitions, with descriptions. There are more extensive guides describing how each is used, and the purpose they serve, in the advanced data modeling guides.
"""
Specifies that a type is a view type.
* space: Overrides the space, which by default is the same as the data model.
* version: Overrides the version of the view, which by default is the same as the data model version.
* rawFilter: Sets a filter on the view, accepts a JSON string.
"""
directive @view(
space: String
version: String
rawFilter: JSON
) on OBJECT | INTERFACE
"""
Specifies that a type is an edge type.
The view can then be used to expand an edge node with a node instance with properties
as specified in the view with the @edge directive.
The edge should be annotated with `@relation(edgeSource: "name_of_edge_view")`.
"""
directive @edge ON OBJECT | INTERFACE
"""
Specifies that a type is importing another view and its backing container.
Currently, this annotation must be combined with @view to specify the
view and view version to import. In order to extend the view in your data
model, specify it as an interface and then create a new data type
that implements the interface and adds additional properties.
In the future, this directive will be extended to simplify importing.
"""
directive @import ON OBJECT | INTERFACE
"""
Overrides the mapping of a field. Can only be used in a view type and can not be used on imported fields.
* space: Overrides the space, which by default is the same as the data model space.
* container: Overrides the container externalId, which by default is the same as the externalId of the view postfixed with 'Container'.
* property: Overrides the container property identifier being mapped.
"""
directive @mapping(
space: String
container: String
property: String
) on FIELD_DEFINITION
"""
Defines the relation field's details
* type: The reference to the edge type this relation is mapped to.
* name: Overrides the name property of the relation definition. This is merely metadata, and should not be confused with the
property identifier!
* edgeSource: The name of the edge view to use for expanding the edge node with a node instance.
* direction: The direction to follow the edges filtered by 'type'.
* type: Specifies the edge type, namespaced by 'space', where the 'externalId' corresponds to the edge type name.
"""
directive @relation(
type: _DirectRelationRef
name: String
edgeSource: String
direction: _RelationDirection
) on FIELD_DEFINITION
"""
Configures the backing container of the view.
It can be used to configure indexes or constraints for the properties of a container.
"""
directive @container(
constraints: [_ConstraintDefinition!]
indexes: [_IndexDefinition!]
) on OBJECT | INTERFACE
"""
Sets the default value of a field.
* value: Specifies the default value. The input is always in string format, however all dms supported default value types are also supported.
"""
directive @default(
value: String
) on FIELD_DEFINITION
"""
Sets the unit on a field.
* externalId: specifies a unit by using an external ID value. `externalId` can only be assigned to the types `Float`, `Float32`, `Float64`, or to an array of listed floating types and must match a unit in the unit catalog.
* sourceUnit: a free text field to store arbitrary unit information like unit name in a source system.
"""
directive @unit(
externalId: String,
sourceUnit: String
) on FIELD_DEFINITION
"""
Represents an index definition.
* identifier: An unique identifier for the index
* indexType: Is the type of index
* fields: List of field names to define the index across. Only supported for non-inherited or mapped fields. The order of the fields matters.
* cursorable: With cursorable index we can efficiently query with custom sort options, and queries will emit cursors that can be used to paginate through the results. Only supported for BTREE indexType.
"""
input _IndexDefinition {
identifier: String!
indexType: _IndexType
fields: [String!]!
cursorable: Boolean
}
"""
Represents a constraint definition. The constraint definition can either be an uniquness constraint or requires.
In case 'constraintType' is set to 'REQUIRES', the 'require' argument must be set.
And if it's set to 'UNIQUENESS', the 'fields' argument must be set.
* identifier: An unique identifier for the constraint
* constraintType: Is the type of constraint
* fields: List of field names to define the unique constraint across. Only supported for non-inherited or mapped fields. The order of the fields matters.
* require: Specify a container (by space and externalId), which is required to exist on the node, if the current container is to be used.
"""
input _ConstraintDefinition {
identifier: String!
constraintType: _ConstraintType
require: _DirectRelationRef
fields: [String!]
}
enum _ConstraintType {
UNIQUENESS
REQUIRES
}
enum _IndexType {
BTREE
}
input _DirectRelationRef {
space: String!
externalId: String!
}
enum _RelationDirection {
INWARDS
OUTWARDS
}
Advanced: mapping and importing fields
A view type can also map directly to properties in a container using the @mapping directive. Thus, the fields of a type defined in GraphQL can be a mix of imported properties and mapped properties.
Fields that are imported via inheritance can not be mapped. Fields that are mapped using the mapping directive, must exist. In other words, there are no auto-generation of fields mapped.
Fields that aren't "imported" are, by default, mapped to a generated container with the same name as the type name. For example above, EquipmentObject
will create a view and a container with the same externalId
.