Objects

Some of the main characteristics of semantic objects include:

  • Having some common props like name, type, __typename.
  • Having a business (GraphQL) type determined through some discriminator (triple pattern).
  • Using props from the properties: list and add or override their characteristics, or define properties directly in props: (see Properties).
  • Using inheritance.

Object Characteristics

Object classes have the following characteristics:

Characteristic Description Default value
name UpperCamelCase (“PascalCase”) symbol. The YAML key. (none)
label Human-readable name name
descr Description or clarification (optional)
typeProp Property that determines the business type rdf:type
type Array of type value IRIs (prefixed, relative, or absolute) name
regex IRI: Unanchored regex pattern (^...$ can be used to anchor it) (none)
prefix IRI: String (not a regex) anchored to the start. A relative IRI can be used. (none)
inherits Class to inherit from (currently, single inheritance is supported) (none)
kind abstract is an abstract superclass object
extend Object extends external object. Generates @extend directives within the GraphQL schema. Allows Semantic Objects to resolve requests to extend external objects entity resolution. false
search Configuration that shows if the object is searchable. Provides different options, which are translated and used to build search index. See more about it here. (none)
sparqlFederatedService The ID of a SPARQL Federation Service. See SPARQL Federated Objects. (none)
meta Adds an additional meta directive in the GraphQL schema for the given object. Should be a valid YAML value. See Custom GraphQL Directives. (none)

Object Defaults

Object characteristics are validated as described below. All of them have sensible defaults, so you only need to specify “name”. For further details, see the following sections.

  • Name (the YAML key): see Naming Convention and IRI Processing.

  • label should be as short as the name. “name” is the default, so you need to give the label explicitly only when you want to provide a more human-readable version.

  • descr is a longer description (optional).

  • typeProp: which RDF property determines the business type, the default is rdf:type (see Object Typing).

  • type: array of type value IRIs to check (prefixed, relative, or absolute). Leaf-level classes have a default: singleton array consisting of the object name, resolved against prefixes and vocab_iri.

    Example of using default type discriminators:

    objects:
      Obj1:
      voc:Obj2:
    

    This has the same result as these explicit type specifications:

    objects:
      voc:Obj1: {type: [voc:Obj1], typeProp: rdf:type}
      voc:Obj2: {type: [voc:Obj2], typeProp: rdf:type}
    
  • name: RDF property designating a single preferred name. The idea is to have a uniform GraphQL prop to access object names regardless of the specific property used for storing.

  • kind: a leaf-level class has kind object, superclasses have kind abstract.

ID (IRI) Generation

The ID of every object is a special value that must be unique. It is also special because it can be generated using regex patterns and a few special functions. There are several options when defining the pattern for the ID in the schema, although the pattern is optional. If a pattern is not defined for some object type in the schema, then the ID for objects of this type will be generated following the default format.

The default ID format is <base_iri>[prefix]<uuid>, where prefix is optional. For example, let’s look at the following schema part:

specialPrefixes:
  base_iri:  http://example.com/data/
objects:
  Human:
    prefix: "human/"
    props: ....

  Droid:
    props: ....

In this case, the generated ID would look as follows:

  • for created Human: http://example.com/data/human/e4cbb674-3da6-4314-92e0-b6ba653085a6
  • for created Droid: http://example.com/data/f110bbed-dd5b-43ee-9a71-ade8ed874f5a

Note that we have the base IRI for both types, but the Human ID also has the prefix for the Human object.

ID Generation Using Prefixes

By using a pattern in the schema for a particular object type, you can define a custom prefix to be used in the generated ID. A custom prefix can be defined as a literal in the pattern, or an already defined prefix/special prefix can be used.

Note

The evaluation of the written pattern must result in an ID starting with http, https, mailto, urn, geo and it must conform to the rules applied for URIs.

Let’s illustrate this with a few examples.

Example 1: Using a predefined prefix

prefixes:
  myPrefix:       http://myprefix.com/
specialPrefixes:
  base_iri:  http://example.com/data/
objects:
  Human:
    pattern: "myPrefix:${uuid()}"
    props:
      gender: {range: string}

Here we are using the custom prefix myPrefix. The generated ID during object creation would look like this: http://myprefix.com/e4cbb674-3da6-4314-92e0-b6ba653085a6.

Example 2: Using a special prefix

specialPrefixes:
  vocab_iri:  https://starwars.org/vocabulary/
  vocab_prefix: voc
objects:
  Human:
    pattern: "voc:${uuid()}"
    props:
      gender: {range: string}

In this example, we are using the vocabulary prefix. The generated ID during object creation would look like this: https://starwars.org/vocabulary/e4cbb674-3da6-4314-92e0-b6ba653085a6. Note that there is no : symbol between the prefix and the uuid.

Example 3: Using a leading literal text in the pattern

specialPrefixes:
  base_iri:  http://example.com/data/
objects:
  Human:
    pattern: "http://myLiteralPrefix.com/${uuid()}"
    props:
      gender: {range: string}

Here we are using a literal text as a prefix. The generated ID during object creation would look like this: http://myLiteralPrefix.com/e4cbb674-3da6-4314-92e0-b6ba653085a6. Like in our previous example, note that there is no : symbol between the prefix and the uuid.

ID Generation Using Other Properties and Literals

The pattern for the ID of created objects allows usage of other properties. Other properties can be referenced inside the pattern like this: ${name}. Other properties can be referenced anywhere in the pattern, and it can contain literals anywhere in it. The only rule is that the final generated ID must be a valid IRI.

Example 1: Using properties inside the pattern

prefixes:
  myPrefix:       http://myprefix.com/
specialPrefixes:
  base_iri:  http://example.com/data/
objects:
  Human:
    pattern: "myPrefix:human/${firstname}/${lastname}/${personalIdentifier}"
    props:
      gender: {range: string}
      personalIdentifier: {range: integer}
      firstname: {range: string}
      lastname: {range: string}

In this case, if a Human is created with personalIdentifier: 121, firstname: "Lando", lastname: "Calrissian", the generated ID would look like this: http://myprefix.com/human/Lando/Calrissian/121. Note that in this pattern, we have mixed a prefix, the literal human, as well as three properties.

Example 2: Using the ID of another object in the middle or in the end of a pattern (with a prefix)

prefixes:
  myPrefix:       http://myprefix.com/
specialPrefixes:
  base_iri:  http://example.com/data/
objects:
  Human:
    pattern: "myPrefix:human/${friend}/${personalIdentifier}"
    props:
      personalIdentifier: {range: integer}
      friend: {range: Human}

In this case, if we assume that we have a Human with ID="http://example.com/data/human/1" and we want to create another Human adding this one as a friend like this:

mutation create_human {
   create_Human(objects: {
    friend: {
      ids: "http://example.com/data/human/1"
    }
    personalIdentifier: 2
  }) {
    human {
      id
    }
  }
}

Then the second Human will have a generated ID=http://myprefix.com/human/human/1/2 as the ID of its friend is used in the pattern.

Note that the prefix here is http://myprefix.com/ and not http://example.com/data. This happens because the referenced object is in the middle of the pattern and the prefix of the referenced object is stripped, so only human/1 remains of its ID.

Example 3: Using the ID of another object in the beginning of a pattern

prefixes:
  myPrefix:       http://myprefix.com/
specialPrefixes:
  base_iri:  http://example.com/data/
objects:
  Human:
    pattern: "${friend}/${personalIdentifier}"
    props:
      personalIdentifier: {range: integer}
      friend: {range: Human}

In this case, if we assume that we have a Human with ID="http://example.com/data/human/1" and we want to create another Human adding this one as a friend like this:

mutation create_human {
   create_Human(objects: {
    friend: {
      ids: "http://example.com/data/human/1"
    }
    personalIdentifier: 2
  }) {
    human {
      id
    }
  }
}

Then the second Human will have a generated ID=http://example.com/data/human/1/2 as the ID of its friend is used in the pattern.

Note that the prefix here is http://example.com/data/. This happens because the referenced object is in the beginning of the pattern and the prefix of the referenced object is not stripped.

ID Generation Using Integrated Functions

There are several integrated functions that can be used in the patterns for ID generation.

  • upperCase() - converts the given text value to upper case.
  • lowerCase() - converts the given text value to lower case.
  • escape() - escapes characters that are not allowed in an ID, such as spaces, quotation marks, and other special symbols. This function is applied by default to all used properties.
  • escape('separator') - escapes special characters using a special given separator. The default separator is -, but can be replaced by something else using this function.

Patterns using these functions may look like this:

pattern: "myPrefix:human/${firstname.upperCase()}/${lastname.escape("_")}

ID Generation Validations

ID generation using patterns provides a lot of flexibility. However, this also means that there is a high chance of the generation of incorrect values that do not conform to the URI format. Two types of errors are possible here:

  • Errors during schema validation - when a schema is created, all patterns inside are evaluated. If there are syntax errors in the pattern, or the pattern evaluation would result in an ID that would not be a valid URI, or it results in a static/constant ID, a validation error is thrown.
  • Errors during objects creation - it is possible that all patterns are valid, so an ID generated by the patterns would also be valid. In some situations, however, a user-entered data may lead to a generated ID that is not a valid URI. In this case, an error during object creation may be thrown.

Object Typing

Semantic object types (business types) correspond to RDF shapes and GraphQL __typename but not RDF types. One restriction of GraphQL is that each object must have exactly one concrete leaf-level type, even though it could implement multiple interfaces. But RDF resources can and very often have multiple rdf:type.

There are many examples where rdf:type does not match the desired business type one-to-one:

  • All Wikidata entities have rdf:type wikibase:Item. You distinguish between them using the prop wdt:P31 “instance of”, e.g., wdt:P31 wd:Q5 indicates “Human”.

  • A typical DBpedia resource has between 10 and 100 rdf:type, but usually one of them is leading or “most significant”.

    For example, the resource for Tim Berners-Lee has 77 types, amongst which the ones listed below. Most of the time, however, you would treat dbo:Person as leading.

dbo:Agent
dbo:Person
dbo:Scientist
schema:Person
dul:Agent
dul:NaturalPerson
owl:Thing
foaf:Person
wikidata:Q5
...
yago:Medalist110304914
...
yago:WikicatBritishComputerProgrammers
...
yago:WikicatFellowsOfTheRoyalSociety
...
yago:WikicatMacArthurFellows
...
yago:WikicatRoyalMedalWinners
...
  • All kinds of thesaurus concepts (lookup values) use rdf:type skos:Concept, and you would usually want to map them to different semantic classes (e.g., Type, Status, Technology, Industry), distinguished by skos:inScheme (thesaurus).
  • All GeoNames resources have rdf:type gn:Feature. The prop gn:featureCode is a 2-level list of over 650 place types such as A.PCLI “independent political entity” (i.e., country) or A.ADM1 “first-order administrative division” (e.g., US state). If want like to capture Country, Region, City as three main geographic object types of interest in a business setting, then you need to map lists of matching featureCode to these business types.

So we decided to use two class characteristics to map RDF values of a node to a business type (semantic object class).

  • typeProp: which RDF property determines the business type, the default is rdf:type.
  • type: array of type value IRIs (prefixed, relative, or absolute) to check. We use an array because sometimes several type IRIs map to the same business type.

typeProp and type together constitute a “type discriminator” to check. They are subject to the following rules:

  • The type lists of objects with the same typeProp should be disjoint to make type discrimination easier. The following counter-example is forbidden because there is a conflict on schema:Person:
objects:
  NotablePerson: {type: [dbo:Person,  schema:Person]}
  SocialPerson:  {type: [foaf:Person, schema:Person]}
  • type values are checked as a disjunction (OR). In the following example, the business type Person gathers instances that have a similarly named rdf:type from three ontologies:
objects:
  Person: {type: [dbo:Person, foaf:Person, schema:Person]}
  • A subclass should satisfy all type discriminators along the inheritance chain, including its own discriminator.

In the following example, we define two lookups as distinct business classes and distinguish by skos:inScheme (i.e., thesaurus). Two conditions will be checked for each lookup value: rdf:type (inherited from Concept) and skos:inScheme (specified in each subclass):

objects:
  ConceptScheme: {type: [skos:ConceptScheme]}
  Concept:       {type: [skos:Concept], props: {skos:inScheme: {range: ConceptScheme}}}
  Industry:      {inherits: Concept, type: [industry/],   typeProp: skos:inScheme}
  Technology:    {inherits: Concept, type: [technology/], typeProp: skos:inScheme}

In the following example, we map GeoNames and three main geographic business types Country, Region, City through lists of gn:featureCode:

objects:
   Geoname:
     regex: '^http://sws\.geonames\.org/\d+/$'
     type: [gn:Feature]
   Country:
     inherits: Geoname
     typeProp: gn:featureCode
     type: [gn:A.PCLI, gn:A.PCLD, gn:A.PCLIX, gn:A.PCLS, gn:A.PCL, gn:A.TERR, gn:A.PCLF]
   Region:
     inherits: Geoname
     typeProp: gn:featureCode
     type: [gn:A.ADM1]
   City:
     inherits: Geoname
     typeProp: gn:featureCode
     type: [gn:P.PPL, gn:P.PPLC, gn:P.PPLA, gn:P.PPLA2, gn:P.PPLA3, gn:P.PPLA4, gn:S.RSTN, gn:S.INSM, gn:L.AREA]
  • If a subclass uses the same typeProp as one of its ancestors, the child discriminator overrides the ancestor discriminator. With the following class definitions, a person that has both rdf:types (e.g., see Tim Berners-Lee above) will be typed with the more specific type Scientist.
objects:
  Person:    {type: [dbo:Person]}
  Scientist: {type: [dbo:Scientist], inherits: Person}
  • A superclass does not have a default type characteristic. It may but does not need to have such a value. For example, the schema.org ontology does not have a common parent class of Person and Organization: see issue schemaorg#700: “The recommended path for those wishing to reflect an Agent in their data would be to identify it as a type schema:Thing. Any further qualification should be with terms from other vocabularies such a foaf:Agent dct:Agent etc in addition to schema:Thing”.) Nevertheless, if we need such superclass in our SOML model, we can have it.
objects:
  Actor:        {kind: abstract}
  Person:       {type: [schema:Person],       inherits: Actor}
  Organization: {type: [schema:Organization], inherits: Actor}

If we want to collect FOAF and schema.org classes into unified “business classes”, we can do it as follows (note: the FOAF ontology has foaf:Agent, but due to discriminator overriding SOML will not check it on instance data):

objects:
  Actor:        {kind: abstract,      type: [foaf:Actor]}
  Person:       {type: [schema:Person,       foaf:Person,     ], inherits: Actor}
  Organization: {type: [schema:Organization, foaf:Organization], inherits: Actor}

Inheritance

We use inheritance to structure the class hierarchy so that:

  • Property ranges can use common superclasses. For example, the Actor superclass above can be used as range of props such as author, creator, contributor, organizer, etc.
  • We reduce the need of repeating properties in similar objects. (Another mechanism is the common list properties.)

Rules:

  • Currently, we support single inheritance (mono-hierarchy), i.e., inherits designates a single superclass.
  • A subclass inherits all properties of its superclass but not characteristics, with two exceptions:
    • Type discriminators are accumulated, see above.
    • The name characteristic is inherited and cannot be changed.
  • Properties in a subclass must fulfill all expectations about the superclass. Therefore there are certain restrictions as to what inherited characteristics you can change (see Characteristic Inheritance)

You can select all instances of a superclass (see Queries). For such query, the Platform collects all subclass discriminators and checks them in a disjunction.

GraphQL schemas do not have inheritance per se, so we implement it as follows:

  • Each superclass is mapped to an interface and SOML inherits is mapped to GraphQL implements.
  • Since one interface cannot inherit another, we expand all inherited interfaces.
  • Since GraphQL types cannot reuse fragments (unlike queries), we expand all inherited fields.

You do not need to bother with these details as the Platform takes care of them in the generated GraphQL schema. However, you need to consider the following limitations:

  • Each GraphQL instance must have a single concrete (leaf-level) type (the __typename).
  • GraphQL interfaces cannot have instances.
  • GraphQL does not allow an interface and a type to have the same name.

We have a characteristic kind that is computed automatically and reflects these limitations:

  • object designates a concrete (leaf-level) class.
  • abstract designates an abstract superclass (GraphQL interface).

If you need to use a concrete superclass, make another superclass and use a naming convention (e.g., by adding suffix Base, Common or Interface) for the abstract superclass. For example, consider modeling Companies and StockExchanges. As you can see, Company inherits CompanyCommon and does not add any props: the sole purpose is to make it a concrete (leaf-level) class.

objects:
  CompanyCommon:
    # kind: abstract # automatically assigned
    props:
      legalName: {min: 1}
      listedOn: {range: StockExchange}
      ticker: {}
      marketCapitalization: {range: decimal}
  Company:
    inherits: CompanyCommon
    # kind: object # automatically assigned
  StockExchange:
    inherits: CompanyCommon
    # kind: object # automatically assigned
    props:
      totalMarketCapitalization: {range: decimal, descr: "Sum of the market cap of all listed companies"}

(For simplicity, we assume that a company can be listed on only one exchange.) In this model, StockExchanges are also Companies and may be listed as well, on themselves or another exchange (listedOn has domain CompanyCommon). Other interpretations argue that StockExchanges are owned and operated by companies but are not themselves companies.

Concrete superclasses (with kind supertype) may be allowed in the future, by implementing such naming convention automatically.

Note

The direct inheritance of internal types or interfaces is prohibited. The restriction was added in order to prevent compatibility issues, because the internal types may evolve alongside the system. This may cause problems if the client schema is not compatible with the changes done in the new version, and may potentially require its complete rework.

The following are considered internal: Nameable, Object, HealthCheck, MutationResponse, Literal, AffectedCount, AffectedObjects, and _Service.

Interface Object

Object instances have some common fields, defined in a GraphQL interface Object:

  • id: each RDF node has exactly one IRI. In the future we may decide to support blank nodes that do not have an IRI, but using such nodes is not a good practice. See Naming Convention and IRI Processing for details.
  • type: the value(s) of rdf:type, zero or more IRIs.
  • __typename: business type, a GraphQL introspection field.
    • It is a string - GraphQL types have no namespaces.
    • It is computed by type discriminators, see Object Typing.
    • It returns only one leaf-level concrete type, not multiple inherited interface types.

This is expressed in GraphQL as follows:

"Common Object interface: provides id, type (rdf:type), __typename"
interface Object {
  "IRI: Each object has exactly one IRI"
  id: ID
  "rdf:type. Most but not all objects have some"
  type: [ID]!
}

The Object interface is the virtual root of the class hierarchy, as all classes are declared to implement it.

Interface Nameable

It is very useful to be able to get a single preferred name of an object. The idea is to have a uniform GraphQL field to access object names, regardless of the specific property used for storing the name.

So we use the class characteristic name to map various RDF props to the same GraphQL field name:

  • This characteristic is optional and there is no default (some objects lack a reasonable name).

  • If used, the RDF property is designated as mandatory single-valued string in that class.

  • So you do not need to define that property explicitly, as it will be generated using the above defaults. If you declare it, make sure it is mandatory and:

    • single-valued string typed. Example: rdfs:label: {min: 1}. (range: string and max: "1" could be omitted as they are defaults)
    • multi-valued with range langString or stringOrLangString (no restrictions on max). Example: rdfs:label: {min: 1, range: langString}.
  • By default, the generated property is marked as nonNullable: false. This can be changed by setting this characteristic to the aliased property itself like in the example below. For more information, see Nullability.

    objects:
      Person:
        name: fullName
        props:
          fullName: {rdfProp: "rdfs:label", min: 1, nonNullable: true, label: "Name"}
    
  • For simplicity, name always returns a plain string (not a Literal).

  • name takes an optional lang: String parameter. For more information, see Language Fetching:

    • It is ignored for datatype string.

    • It cannot use the ALL mode because name should return a single value.

    • If the lang.fetch spec of the designated property, or the effective lang.fetch option in the query:

      • Includes ALL: this mode is ignored.

      • Has a non-empty string value: it will be used to reduce the returned value to a single one based on that. The same value will be used as parent object filter if such is not already defined.

      • Has no value or an empty one: lang: ANY will be used to ensure that the property has a value.

      • The described behavior can be changed by using the schema level configuration like this below:

        config:
          lang: {defaultNameFetch: "ANY", appendDefaultNameFetch: true}
        
        • defaultNameFetch: can be used to change the default pattern used for name loading from the default ANY to something more suitable like en~,NONE,~ (any English “dialect”, plain string, or any lang). The pattern will be used differently based on the appendDefaultNameFetch configuration value. Note that this value can be disabled by setting null or empty string, but as a failsafe ANY will be used for the name loading, as otherwise all values will be loaded from the database.

          Warning

          If you are planning to execute queries with high depth (more than 6-7 levels of nesting) and the mapped name property has many values in different languages (more than 30), you may experience performance degradation if the used filters are very complicated.

          Warning

          Do not use NONE if you do not expect the mapped name property to have values without a language as it will cause slow query times.

        • appendDefaultNameFetch: specifies how the defaultNameFetch should be used. If true, the pattern will be appended to any user-defined patterns to ensure a value would be returned for the name property. If false, the pattern will be used only if the user does not specify schema, query, and property lang fetch spec. In this case, if the dataset does not contain a value matching the user criteria, a null will be returned for the name.

  • The name property is marked as read-only. This means that it is excluded from all mutations. If the value needs to be updated, it should be done via the original assigned property. For more information about read-only properties, see Immutability.

    Warning

    This functionality is introduced with version 3.2.0 of the Ontotext Platform. Applications using earlier versions need to be updated so that all queries that mutate the name property directly mutate the referenced property instead.

    The following examples show the changes that need to be done in order to migrate any existing queries that mutate the name property:

    • For single-valued string properties like the example below, the changes for the create and update mutations are as follows:

      objects:
        Person:
          name: fullName
          props:
            fullName: {rdfProp: "rdfs:label", min: 1, label: "Name"}
      
      • Create mutation:

        mutation {
          create_Person(objects:[{
            name: "John Doe"
          }]) {
            person {
              id
              name
            }
          }
        }
        

        should be changed to

        mutation {
          create_Person(objects:[{
            fullName: "John Doe"
          }]) {
            person {
              id
              name
            }
          }
        }
        
      • Update mutation:

        mutation {
          update_Person(where: {name: {EQ: "John Doe"}}, objects: {
            name: "John Richards"
          }) {
            person {
              id
              name
            }
          }
        }
        

        should be changed to

        mutation {
          update_Person(where: {name: {EQ: "John Doe"}}, objects: {
            fullName: "John Richards"
          }) {
            person {
              id
              name
            }
          }
        }
        
    • For multi-valued langString or stringOrLangString properties like the example below, the changes for the create and update mutations are as follows:

      objects:
        Country:
          name: rdfs:label
          props:
            rdfs:label: {range: langString, min: 1, max: inf, label: "Name", lang: { implicit: "en"} }
      
      • Create mutation:

        mutation {
          create_Country(objects:[{
            name: "Bulgaria"
          }]) {
            country {
              id
              name
            }
          }
        }
        

        should be changed to

        mutation {
          create_Country(objects:[{
            id: "https://www.geonames.org/732800"
            rdfs_label: [
               {value: "Bulgaria"},  # default lang is "en"
               {value: "Bilgari", lang: "ht"}
            ]
          }]) {
            country {
              id
              name
            }
          }
        }
        
      • Update mutation:

        mutation {
          update_Country(where: {ID: ["https://www.geonames.org/732800"]}, objects: {
            name: "Bulgarien"
          }) {
            country {
              id
              name
            }
          }
        }
        

        should be changed to

        mutation {
          update_Country(where: {ID: ["https://www.geonames.org/732800"]}, objects: {
            rdfs_label: [
              {
                value: [{
                  {value: "Bulgarien", lang: "de"}
                }]
              }
            ]
          }) {
            country {
              id
              name
            }
          }
        }
        

If a class uses the name characteristic, it is declared to implement the Nameable interface. This is expressed in GraphQL as follows:

"Nameable interface: provides name"
interface Nameable {
  "Single pref name of an object to represent it. This abstracts from the specific RDF prop used as a label, eg x:prefName, skos:prefLabel, rdfs:label"
  name(lang: String = "NONE,en~,~"): String @constraints(minCount : 1, maxCount : 1)
}

Name Example

Consider the following SOML schema:

config: {lang: "BROWSER,ANY"}  # first Accept-Header preference, and then any value
objects:
  skos:Concept:
    name: skos:prefLabel
    props:
      skos:prefLabel:  {max: inf, range: stringOrLangString, min: 1, lang: {validate: "UNIQ"}}
      skos:definition: {max: inf, range: stringOrLangString}

Let’s assume the BROWSER Accept-Language is set to "fr, fr-CA".

This will effectively set the schema lang.fetch spec to "fr-CA~,fr~,ANY" because Accept-Language uses prefix matching semantics, and the more specific language is sorted first.

Assume data like this:

<https://nomenclature.info/nom/5313>
  a skos:Concept;
  skos:prefLabel
    "Two-Handed Crosscut Saw"@en,
    "Godendard"@fr-CA,
    "Passe-partout"@fr;
  skos:definition
    "A tool consisting of a stationary, wide, heavy, serrated blade..."@en,
    "Outil composé d'une longue lame dentelée à dos convexe..."@fr.

You can use a query like this to fetch a concept with its single name according to schema preferences, and all of its available labels and descriptions in any language.

query name_allLabels_allDescr {
  skos_Concept(ID:"https://nomenclature.info/nom/5313") {
    name
    skos_prefLabel (lang:"ALL") {value lang}
    skos_definition(lang:"ALL") {value lang}
  }
}

Then the following response is returned. Please note that only one value of name is returned, and it is a plain string (not array of literals), but all available values of prefLabel (which feeds name) and definition are returned.

{
  "data": [{
    "skos_Concept": {
      "name": "Godendard",
      "skos_prefLabel": [
        {"value": "Two-Handed Crosscut Saw", "lang": "en"},
        {"value": "Godendard",               "lang": "fr-CA"},
        {"value": "Passe-partout",           "lang": "fr"}
      ],
      "skos_definition": [
        {"value": "A tool consisting of a stationary, wide, heavy, serrated blade...", "lang": "en"},
        {"value": "Outil composé d'une longue lame dentelée à dos convexe...",         "lang": "fr"}
      ]
    }
  }]
}

You can also request name in a specific language:

query frName {
  skos_Concept(ID:"https://nomenclature.info/nom/5313") {
    name(lang:"fr")
  }
}

This is the response:

{
  "data": [{
    "skos_Concept": {
      "name": "Passe-partout"
    }
  }]
}

Finally, if you request name in a non-existent language, the fallback is used:

You can also request name in a specific language:

query fallbackName {
  skos_Concept(ID: "https://nomenclature.info/nom/5313") {
    name(lang:"bg")
  }
}

This is the response:

{
  "data": [{
    "skos_Concept": {
      "name": "Two-Handed Crosscut Saw"
    }
  }]

}

As we display a single value for the name but allow it to point to multiple triples in the database, sometimes slightly confusing results might be returned. For example:

  • Filtering by name value, but not getting the same value in the response:

    query conceptName {
      skos_Concept(where: {name: {IRE: "Two-Handed Crosscut Saw"}}) {
        name
      }
    }
    

    Will result in a response like the one below as the default language is Canadian French:

    {
      "data": [{
        "skos_Concept": {
          "name": "Godendard"
        }
      }]
    }
    
  • Filtering by absolute value may not work as expected. The example below will not perform the filtering via the literal index but rather use string equality:

    query conceptName {
      skos_Concept(where: {name: {EQ: "Two-Handed Crosscut Saw"}}) {
        name
      }
    }
    

If new values need to be added to the skos:Concept name property, it should be done via the mapped skos:prefLabel as follows:

mutation updateConcept {
  update_skos_Concept(where: {ID: {EQ: "https://nomenclature.info/nom/5313"}},
       objects:{
          skos_prefLabel: {
            value: {
              {value: "Бичкия", lang: "bg"}
            }
          }
       }) {
    skos_concept {
      name
    }
  }
}

SPARQL Federated Objects

SPARQL Federated Objects are a lightweight way of linking the data from multiple SPARQL repositories using SPARQL Federation in a single SOML schema.

Configuration

If an object has sparqlFederatedService configured, it will be treated as an external one and retrieved over the SPARQL Federation protocol. For example:

FilmRole:
  sparqlFederatedService: wikidata

This means that when you query FilmRole, the data will be retrieved from the wikidata service.

Warning

Before using a federated service, you should first declare it.

Federated and non-federated objects can be mixed together. For example:

Character:
  props:
    role: {range: FilmRole}

FilmRole:
  sparqlFederatedService: wikidata
  props:
    character: {range: Character}

Here, Character is a non-federated object, while FilmRole is a federated one. Notice how both objects have properties with ranges outside of their own service. Each object can have properties with ranges local object, federated objects in the same service or federated objects in a different service.

In the query below, the data about character is collected from the local repository, while the data for role is collected from the federated wikidata service:

query character {
  character {
    id
    role {
      id
      roleType
    }
  }
}

Note that the triple linking character and role is expected to be in the character object - therefore in the local repository.

Limitations

The SPARQL Federated Objects have some limitations:

  • The Federated Objects are read-only. No mutations will be exposed over them.
  • Unlike Apollo Federation, in order to use SPARQL Federated Objects, all datasources should be SPARQL-based.
  • SPARQL Federation can have a huge performance impact on the queries. A single GraphQL query (especially if it has deeper hierarchy and used limits and order) could be split internally into multiple SPARQL queries. If these queries are federated, then GraphQL will have to perform multiple calls to the external services.
  • It is not allowed to configure an inverseAlias property on a Federated Object whose range is a Non-Federated Object.

See this tutorial for more details on how to configure and use the Federated SPARQL Objects.

Custom GraphQL Directives

Directives in the GraphQL schema can be used to pass information to the GraphQL consumer without affecting the actual GraphQL behavior. They are completely ignored when performing queries and mutation, but can be accessed via introspection.

For example, let’s say we have a UI that consumes the GraphQL schema and displays the objects defined in the SOML schema. As we know, the object IDs cannot contain spaces, so an object representing a film role would be named FilmRole. However, in the UI we would like this object be named Film Role. So what we can do is:

FilmRole:
  meta: {displayName: "Film Role"}

This additional directive does not affect queries and mutations over the FilmRole object. However, it can be retrieved using introspection:

query {
  __type (name: "FilmRole") {
    name
    appliedDirectives (name: "meta") {
      name
      args {
        name
        value
      }
    }
  }
}

This query will return:

{
  "data": {
    "__type": {
      "name": "FilmRole",
      "appliedDirectives": [
        {
          "name": "meta",
          "args": [
            {
              "name": "_",
              "value": "{displayName : \"Film Role\"}"
            }
          ]
        }
      ]
    }
  }
}

Note

The appliedDirectives field on the __Type and __Field GraphQL types is not part of the GraphQL spec. However, it is added as a feature in graphql-java and further extended to support the name argument.

The meta characteristic of a class is calculated as a union of its own meta characteristic and those of its parents. For example, in the following hierarchy:

Parent:
  kind: abstract
  meta: {parentMeta: true}

Child:
  inherits: Parent
  meta: {childMeta: true}

Child2:
  inherits: Parent
  meta: {parentMeta: false}

In the GraphQL schema, for the Child object, both parentMeta and childMeta will be present in the meta directive. At the same time Child2 will overwrite the parentMeta property from the Parent object and will end up with {parentMeta: false}.

Custom directives can be configured on Property level as well.