Domain configuration
Entities
Attribute configuration

Attribute Configuration

Entities have attributes describing the data of the entity. You can define its aspects by the this configuration type, in YAML, JSON or a configuration object. In the following examples we will prefer YAML and use a configuration object only when necessary.

Configuration Type

parametertypepurpose
typestringtype of attribute values, can be any GraphQL or ActiveQL scalar or any of your defined enums
requiredbooleanmandatory attribute value; adds schema and business validations for non-null values
uniquebooleanuniqueness of value; adds business validation for unique values, also within a scope
patternstringadds business validation to check if the value matches this pattern
listbooleanlist of scalar types
defaultValueany or Functionstatic or dynamic default values for new entity items
filterTypestring or booleandisable or change filter behavior for attributes
descriptionstringadding documentation to the public API / schema
validationobjectconfigure business validation using extensive ValidateJS syntax
resolveFunctioncallback to determine custom value for a field that will be send to a client
virtualbooleannon-persistent value; value is never written or read from datastore
objectTypebooleanshould this attribute be part of the type itself (or only mutations)
mediatypestringonly used as metadata for UI clients, e.g. ActiveQL Admin UI
sequencenumberany number > 0 will be used as a start for a sequence, any next item will get the next sequence value

Shortcut Notation

Instead of providing the configuration object you can skip everything and just write the type as value for the attribute instead. The rest of the attribute configuration object would then be set as default. You can even use all type shortcut notations (such as String! or Key as described below) when using this. The following examples are equivalent:

Shortcut More explicit notation
entity:
  Car: 
    attributes:
      brand: String!
      mileage: Int
      license: Key
entity:
  Car: 
    attributes:
      brand: 
        type: String
        required: true
      mileage: 
        type: Int
      license: 
        type: String
        required: true
        unique: true
        updateInput: false

type

ConfigDescription
stringActiveQL will
  • try to resolve all shortcuts; then
  • try to identify any GraphQL scalar type; then
  • try to identify an known Enum
  • otherwise it's assumed to be the name of a GraphQL type
string[]any list of strings will be resolved into an Enum

While technically possible you should not use an Entity type as a type for an attribute but instead describe the relationship between entities as associations.

Type Shortcuts

The most common attribute configurations can be done by via shortcut notation:

ValueDescription
Keysets the type to String, required and unique to true
^pattern$sets the type to String and the pattern to pattern
typeName!sets the type to 'typeName' and required to true, e.g. Int! becomes { type: 'Int, required: true}; works with other shortcuts too
[typeName] or typename[]sets type to 'typeName' and list to true, e.g. Int! becomes { type: 'Int, list: true }
urlsets type to String and a pattern validation to a valid URL
Int+ | Float+adds a validation numbers greater than 0
Int- | Float-adds a validation numbers lesser than 0
Float.nadds a decimal of n places to the Float attribute
string[]if the type definition is list of strings, an enum will be created and the type of the attribute will be of this enum

GraphQL scalar type

You can use any GraphQL scalar type. Check also GraphQL Type System (opens in a new tab)

ValueDescription
IntA signed 32-bit integer.
FloatA signed double-precision floating-point value.
StringA UTF-8 character sequence.
Booleantrue or false
StringA UTF-8 character sequence.
IDrepresents a unique identifier, Although allowed it is advised not to use the ID type since ActiveQL uses this to identify entity items and establish relations between entities (think primary and foreign keys).

ActiveQL scalar types

In addition to the GraphQL scalar types, ActiveQL provides the following types, that can be used as a type for an attribute.

ValueDescription
DateTimeString representation of a Date in the JSON data it serializes to/from new Date().toJSON() internally it converts it to a Javascript Date object
DateString representation of a Date in simplified format yyyy-mm-dd; this type does not know about any timezones, it is merely a structured string, nonetheless sufficient in many situation when you don't care about timezones or complex date calculations. If you do, use DateTime instead.
JSONarbitrary JSON structure (you should use this with caution and prefer GraphQL types instead); this is also the fallback type when a type cannot be determined to keep a configuration executable

Enum

An attribute with an enum type (either an enum from configuration, schema or inline) will become a GraphQL enum, meaning the GraphQL layer will only accept requests and responses with values of this enum.

ValueDescription
enumNamewill take this (enum) if exists as type for this attribute
List of valueswill create an enum on the fly in the graph

Instead of an Enum name you can also simply state a list of values. An Enum will be created on the fly. So the following configuration would result in the same graph.

enum: 
  CarBrandEnum: 
    - Mercedes
    - BMW
    - Porsche
entity: 
  Car: 
    attributes: 
      brand: CarBrandEnum
entity: 
  Car: 
    attributes: 
      brand: 
        - Mercedes
        - BMW
        - Porsche

required

ValueShortcutDescription
falseif not providedno effect
trueattributeName!NonNull in entity object and create input type, required added to validation

Non-Null Field in Schema

If you set the required modifier for an attribute to true, the corresponding field of the following types become a NonNull field in the GraphQL schema:

  • the entity object type
  • the input type for the create mutation

This means a client not providing a value for this field in a create mutation would result in a GraphQL error. Since the "required-requirement" is part of the public API you can expect any client to handle this correctly. If you prefer to send ValidationMessages (instead of throwing an error) when a client sends null-values for required fields in a create mutation, you can leave the required option to undefined or false and use an attribute validation instead.

Please be aware there will also be an error thrown by the GraphQL layer if a resolver does not provide a non-null value for a required attribute. As long as the data are only handled by the default mutations or the EntiyAccessor, this should never happen. But it could be the case if the data in the datastore are manipulated directly by a custom query or mutation or external source.

Attribute Validation

In addition to the non-null schema field a required validation is added to the validation of any required attribute. You might ask why, since the GraphQL layer would prevent any non-null value anyhow. This is only true for the create mutation. The input type for the update mutation allows a client to only send the attributes that should be updated. So even the field for a required attribute in the update input type allows a null-value - to leave it untouched. But now a client could erroneous send something like {brand: null} even if "brand" is a required attribute.

In addition to this any custom mutation could (and should) use an entity to create or update entity items. These values are not "checked" by the GraphQL schema of course. Therefore before saving an entity item, all validations - incl. this required - validation must be met.

Meta Data

The information will also be part of the MetaData and can therefore used by any UI client. E.g. the ActiveQL Admin UI uses this information to render a mandatory input field for this attribute.

Example

YAML Resulting Schema (excerpt)
entity:
  Car: 
    attributes:
      brand: 
        type: String
        required: true

same as short

entity:
  Car: 
    attributes:
      brand: String!
type Car {
  id: ID!
  brand: String!
  createdAt: DateTime!
  updatedAt: DateTime!
}
 
input CarCreateInput {
  brand: String!
}
Request Response
mutation {
  createCar( car: { } ){
    car{ id brand }
    validationViolations 
  }
}
{
  errors: [
    {
      message: 'Field "CarCreateInput.brand" of required type "String!" was not provided.'
    }
  ]
}

list

list?:boolean
ValueShortcutDescription
false(default)no effect
true[attributeName]type of this attribute is a list of the scalar type

Setting list to true will set the field for this attribute in the following schema types as a GraphQLList type:

  • for the entity object type
  • for the input type for the create mutation
  • for the input type for the update mutation
  • for the input type for the types query filter

Please be aware that your datastore implementation might not be able to handle this or at least makes it harder or impossible to filter or sort these attributes. The default datastore implementations uses a document database and will therefor store arrays in the entity item document quiet easily.

Also note it is only possible to set one required configuration per attribute. If true it is treated as setting the list values to a NonNull type, but never the list field itself. So you can set the configuration to [String!] or explicit { type: 'String', required: true, list: true } and the resulting field type of the expected GraphQL type [String!]. But you cannot express a configuration that would lead to a schema field type of [String]! or [String!]!. In other words: list scalar fields are never required.

Example

YAML Configuration Schema (excerpt)
entity: 
  Car: 
    attributes: 
      license: Key
      repairsAtKm: 
        type: Int
        required: true
        list: true

same as short

entity: 
  Car: 
    attributes: 
      license: Key
      repairsAtKm: [Int!]
type Car {
  id: ID!
  license: String!
  repairsAtKm: [Int!]
  createdAt: DateTime!
  updatedAt: DateTime!
}
 
input CarCreateInput {
  license: String!
  repairsAtKm: [Int!]
}
 
input CarFilter {
  id: IDFilter
  license: StringFilter
  repairsAtKm: IntFilter
}
 
enum CarSort {
  license_ASC
  license_DESC
  repairsAtKm_ASC
  repairsAtKm_DESC
  id_ASC
  id_DESC
}
 
input CarUpdateInput {
  id: ID!
  license: String
  repairsAtKm: [Int!]  
}

As you see the regular filter and sort types are used. Also please note that the repairsAtKm field for the update type is a NonNull type. This is a client can decide to not provide a list for an update - the current values would be left untouched. But when a list is updated is must meet the required configuration.

You can filter List scalar the same way you would filter a regular field, instead it uses any entry in the list to match against the filter. The same goes for sorting. Whey you sort after a list attribute the max/min, first/last entry is used (depending on the type) to find the sorted position of an entity item.

If you need more control over how you want to filter or handle these kind of data we strongly suggest to model these as separate entities with associations with each other.


decimal & decimalPolicy

Float values can have an arbitrary number of decimal places. You can how many you want to accept for a certain attribute.

decimal?:number
decimalPolicy?:'reject'|'round'
decimalDescription
Float values are kept how they wer sent by a client
numberdepending on decimalPolicy the Float values are rounded to these number of decimal places
decimalPolicyDescription
'round'Float values are rounded to these number of decimal places
'reject'if a Float value has more decimal places than the decimal configuration - a validation violation occurs

Example

entity: 
  Car: 
    attributes: 
      value1: Float
      value2: Float.2
      value3: 
        type: Float
        decimal: 2
      value4: 
        type: Float
        decimal: 1 
        decimalPolicy: reject
attribute
value1regular Float no roundin or validating
value2will round any float value from a client to 2 decimal places
value3same as Float.2
value4adds a ValidationViolation when a client sends a float value with more than 4 decimal places

unique

unique?:boolean|string
ValueShortcutDescription
false(default)no effect
trueadding validation of uniqueness of this attribute to the entity
[other attribute]adding validation of uniqueness of this attribute to the entity within the scope of this attribute
[assocTo Name]adding validation of uniqueness within the scope of the assoc of this attribute to the entity

If an attribute is declared as unique, a validation is added to check that no entity item with an equal value for this attribute exists. If it finds the input value not unique it adds a message to the ValidationViolaton return type.

If the attribute is not required - it would allow many null values though.

Example

Let's assume we want to express the requirement that the license number of a car should be unique. We could write

entity: 
  Car: 
    attributes: 
      brand: String!
      license: 
        type: String
        unique: true
Request Response

Let's see what cars already exist.

query {
  cars { id brand license }
}

One car with the license number "HH-BO 2020" exists.

{
  "data": {
    "cars": [
      {
        "id": "5faac51a51434df073bb2dad",
        "brand": "Mercedes",
        "license": "HH-BO 2020"
      }
    ]
  }
}

If we try to create a 2nd car with the same license ...

mutation{
  createCar( car: { 
    brand: "BMW", 
    license: "HH-BO 2020" 
  }) {
    car{ id brand license }
    validationViolations 
  }
}

... we would get a validation violation.

{
  "data": {
    "createCar": {
      "car": null,
      "validationViolations": [
        {
          "path": "license",
          "message": "value 'HH-BO 2020' must be unique"
        }
      ]
    }
  }
}

Adding a car with a different license ...

mutation{
  createCar( car: { 
    brand: "BMW", 
    license: "HRO-TR 1970" 
  }) {
    car{ id brand license }
    validationViolations 
  }
}

... passes all validations.

{
  "data": {
    "createCar": {
      "car": {
        "id": "5faac61251434df073bb2db0",
        "brand": "BMW",
        "license": "HRO-TR 1970"
      },
      "validationViolations": []
    }
  }
}

Scoped attribute unique

Sometimes a value must only be unique in a certain scope. Let' assume we want to make sure that there are no two cars with same color of the same brand.

entity: 
  Car: 
    brand: String
    color: 
      type: String
      unique: brand
Request Response

Let's see what cars already exist.

query {
  cars { id brand color }
}

There is a red Mercedes.

{
  "data": {
    "cars": [
      {
        "id": "5faac94c7dc3c1f1d9a7bf6f",
        "brand": "Mercedes",
        "color": "red"
      }
    ]
  }
}

Let's try to create another red Mercedes.

mutation{
  createCar( car: { 
    brand: "Mercedes", 
    color: "red" 
  }){
    car{ id brand color }
    validationViolations 
  }
}

Validation does not pass.

{
  "data": {
    "createCar": {
      "car": null,
      "validationViolations": [
        {
          "path": "color",
          "message": "value 'red' must be unique within scope 'brand'"
        }
      ]
    }
  }
}

A red BMW though ...

mutation{
  createCar( car: { 
    brand: "BMW", 
    color: "red" 
  }){
    car{ id brand color }
    validationViolations 
  }
}

... is created without objection.

{
  "data": {
    "createCar": {
      "car": {
        "id": "5faac9cf7dc3c1f1d9a7bf70",
        "brand": "BMW",
        "color": "red"
      },
      "validationViolations": []
    }
  }
}

Scoped assocTo unique

If you have an assocTo relation to another entity - you can also scope the unique to this association, by using the fieldName of the assocTo association (default: the typeQueryName of the associated entity).

Lets assume we have multiple vehicle fleets and a car belong to exactly one vehicle fleet. A car should manually assigned order number to express a rating. This rating should be unique for a fleet, only one car should be on position 1, 2, 3 etc.

Making the attribute "rating" unique would prevent to have independent rating numbers per fleet. So you can scope this unique option to the assocTo relationship.

entity: 
  VehicleFleet: 
    attributes: 
      name: String!
  Car:
    assocTo: VehicleFleet
    attributes: 
      license: Key
      brand: String!
      nickname: 
        type: String
        required: true
        unique: vehicleFleet

pattern

pattern?:string

You can add a business validation for strings to match a certain pattern. This validation is independent from the configured validation framework. The pattern must be enclosed in ^ and $.

entity: 
  Book:
    attributes: 
      title: Key
      isbn:
        pattern: ^(?=(?:\D*\d){10}(?:(?:\D*\d){3})?$)[\d-]+$

Note that regardless of any configured type it will always be string, so it's safe to omit the type.

It is recommended to use the shortcut notation:

entity: 
  Book:
    isbn: ^(?=(?:\D*\d){10}(?:(?:\D*\d){3})?$)[\d-]+$

You can also use (only in shortcut) the required identifier !.

entity: 
  Book:
    isbn: ^(?=(?:\D*\d){10}(?:(?:\D*\d){3})?$)[\d-]+$!

would be the same as

entity: 
  Book:
    isbn:
      pattern: ^(?=(?:\D*\d){10}(?:(?:\D*\d){3})?$)[\d-]+$
      required: true

defaultValue

defaultValue?:any|(( attributes:any, runtime:Runtime)=>any|Promise<any>)
ValueShortcutDescription
[empty](default)no effect
[any value]default value when creating a new entity item
[Function]called to get the default value for a new entity item; can return a value or a Promise

You can set either a value or a callback function (configuration object only) to determine a default value for an attribute if a client does not provide a value for it. There will be some checks if the value matches the type of the attribute and some sanitizing if possible. But you should be aware of the correct type since it could come to unwanted unwanted casts or errors if it doesn't.

If you provide defaultValue (literal or function) in the configuration, this attribute becomes no longer mandatory in the CreateInputType schema type. Since there will always be a default value the required condition will be met when creating a new items even when a value is not provided by a client.

Default Value Example

Let's assume any new car should have a mileage of 0 and the color white. Notice how the required attribute "mileage" remains a NonNull field in the Car schema type but no longer in the CarCreateInput type.

YAML Configuration Schema (excerpt)
entity: 
  Car: 
    attributes: 
      brand: String!
      registration: 
        type: Date
        default: white
      mileage:
        type: Int!
        default: 0
type Car {
  id: ID!
  brand: String!
  color: String
  mileage: Int!
  createdAt: DateTime!
  updatedAt: DateTime!
}
 
input CarCreateInput {
  brand: String!
  color: String
  mileage: Int
}
 
input CarUpdateInput {
  id: ID!
  brand: String
  color: String
  mileage: Int
}

Sometimes we need dynamic default values. Let's say the registration date of a car should be set to today when not provided by a client. We could not add a static value for that - so we use the callback. We do not use the runtime in this implementation - but it could be used to access other entities or access a 3rd party API or anything else. We cant add the callback function in YAML but it is totally ok to have the entity definition in a YAML configuration and only add the specific attribute option in a configuration object.

{
  entity: {
    Car: {
        registration: {
          type: 'Date!',
          defaultValue: (rt:Runtime) => new Date()
        }
      }
    }
  }
}
Request Response
mutation { 
  createCar( car: { brand: "Mercedes" } ){
    car { id brand registration }
  }
}
{
  "data": {
    "createCar": {
      "car": {
        "id": "5fac51ca22e89a4ed29e172e",
        "brand": "Mercedes",
        "registration": "2020-11-11"
      }
    }
  }
}
mutation { 
  createCar( 
    car: { 
      brand: "Mercedes", 
      registration: "2019-12-03" } 
    ){
    car { id brand registration }
  }
}
{
  "data": {
    "createCar": {
      "car": {
        "id": "5fac554ac0c9164fcce7530e",
        "brand": "Mercedes",
        "registration": "2019-12-03"
      }
    }
  }
}

filterType

filterType?:string|false
ValueShortcutDescription
[empty](default)attribute will be added to the entity filter type if a default filter for the attribute type exists
falseattribute will not be added to the entity filter type
'filterName'attribute will be added to the entity filter type if filter type "filterName" exists

Usually every attribute will be added to the filter type for the entity, so a client could filter or search for entity items over this attribute's values. This is true with the exception of

  • File
  • JSON

For any other attribute it is tried to determine a filter type per convention [TypeName]Filter so e.g. for the field type String a filter type StringFilter is used. These FilterTypes must come from the datastore implementation, since they are in their behavior dependent on how a datastore gathers data.

The default ActiveQL datastore implementations provide the following FilterTypes:

  • IDFilterType
  • StringFilterType
  • StringListFilterType
  • IntFilterType
  • FloatFilterType
  • BooleanFilterType
  • DateFilterType
  • DateTimeFilterType

Also for any Enum type a FilterType is added. So if you have an enum CarBrand the filter type CarBrandFilter will be generated.

If you want to prevent to filter / search for a certain attribute you can set the filter configuration for this attribute to false.

If your datastore implementations offers more or other filter types you can also override the convention by declaring the filter type name here.

For more information how to use filter check out the Filter/Search section in Queries and Mutations.

Filter Type Example

Let's assume we do not want to allow a client to filter cars by their "brand", only by its other attributes ("mileage" or "color"). We will set the filter for "brand" to false. Notice how the "brand" is no longer part of the CarFilter type.

Object Configuration Schema (excerpt)
{
  entity: {
    Car: {
      attributes: {
        brand: {
          type: 'String!',
          filterType: false
        },
        color: 'String!',
        mileage: 'Int!'
      }
    }
  }
}
type Car {
  id: ID!
  brand: String!
  color: String!
  mileage: Int!
  createdAt: DateTime!
  updatedAt: DateTime!
}
 
input CarFilter {
  id: IDFilter
  color: StringFilter
  mileage: IntFilter
}

description

description?:string

You can add any information / documentation to an attribute that will become part of the schema documentation. In some circumstances ActiveQL adds some description itself (e.g. the validation information) but will always leave your description intact.

Description Example

YAML Configuration Schema (excerpt)
entity:
  Car:
    attributes:
      brand: String!
      color:
        type: String
        description: >
          this is not really 
          evaluated anywhere
          and just informally 
          collected

(We use the standard YAML feature of multiline text input here.)

type Car {
  id: ID!
  brand: String!
  # this is not really evaluated anywhere and just informally collected
  #
  color: String
  createdAt: DateTime!
  updatedAt: DateTime!
}
 
input CarCreateInput {
  brand: String!
  # this is not really evaluated anywhere and just informally collected
  #
  color: String
}
 
input CarFilter {
  id: IDFilter
  brand: StringFilter
  color: StringFilter
}
 
input CarUpdateInput {
  id: ID!
  brand: String
  # this is not really evaluated anywhere and just informally collected
  #
  color: String
}

Schema Documentation

Color Description


validation

validation?:object
ValueShortcutDescription
[empty](default)no validation will be added (defaults like required are not influenced)
objectvalidation configuration for the current Validator instance, default uses ValidateJS

Validations take place before saving an entity item. If validation, either any attribute-validation or entity-validation returns something different then undefined or [] the validation fails and no create or update happens. The validations create a list of ValidationViolation that informs the client about the failed validations.

Please notice that these attribute validations are only applied when a potential required validation did not fail before. This is certainly the case if triggered by a GraphQL request, since the GraphQL layer already correct non-null values, but also wenn used by any custom code. In other words only non-values values will be validated.

Any validation configuration is added as stringified JSON to the description of an attribute, thus becoming part of your public API documentation. It is also provided as MetaData so any UI client (as the ActiveQL Admin UI) could use this for client-side validation.

ValidateJS

The default EntityValidation uses ValidateJS for configurable validations of attribute values. If you decide to use another validation library (by providing another EntityValidation implementation) you should use the syntax of the chosen library then.

For ValidateJS syntax check out their extensive documentation (opens in a new tab).

Validation Function

For non-trivial validations not expressible by configuring ValidateJS validation, you can always implement a callback function on the entity definition and implement any custom validation logic there. See Entity Validation

Attribute Validation Example

Let's assume we want to ensure that the "brand" of a car item as at least 2 and max 20 characters, also the "mileage" should be a positive number. The "brand" is required, we do not need to add a validation for this, since the type is already indicating this attribute as required. The "mileage" is optional though but if provided must match the validation.

YAML Configuration Schema Doc Viewer

We can ValidateJS syntax in yaml.

entity: 
  Car: 
    attributes: 
      brand: 
        type: String!
        validation: 
          length: 
            minimum: 2
            maximum: 20
      mileage: 
        type: Int
        validation:
          numericality: 
            greaterThan: 0
            lessThanOrEqualTo: 500000    

The stringified JSON is added to the description of the field in the schema.

Validation Description

Request Response

If we now try to create car item with invalid values ...

mutation { 
  createCar( car: { brand: "X" mileage: 0 } ){
    car { id brand mileage }
    validationViolations 
  }
}

... we get the ValidationViolations and no car item was created.

{
  "data": {
    "createCar": {
      "car": null,
      "validationViolations": [
        {
          "path": "brand",
          "message": "Brand is too short (minimum is 2 characters)"
        },
        {
          "path": "mileage",
          "message": "Mileage must be greater than 0"
        }
      ]
    }
  }
}

resolve

resolve?:(arc:AttributeResolveContext) => any
 
export type AttributeResolveContext = {
  item:any
  resolverParams:ResolverParams
  runtime:Runtime
  principal:PrincipalType
}

Any attribute resolve function is called on every entity item before it is delivered to an API client. Any value returned by the resolve functions becomes the attribute value in the entity item in the following results

  • typeQuery
  • typesQuery
  • return of create mutation
  • return of update mutation
  • embedded items for assocTo, assocToMany and assocFrom relationships

You get (amongst others) the item as it comes from the datastore in the AttributeResolveContext argument.

attribute Resolve Example

Let's assume you want to deliver the "brand" of a car always in upper letters, regardless how it is stored in the datastore.

{
  entity: {
    Car: {
      attributes: {
        brand: {
          type: 'String!',
          resolve: ({item}) => _.toUpper( item.brand )
        }
      }
    }
  }
}
Request Response
mutation createCar { 
  createCar( car: { 
    	brand: "Mercedes" } 
  	){
    car{ id brand }
    validationViolations 
  }
}
{
  "data": {
    "createCar": {
      "car": {
        "id": "5fcf8820c878031c94d18ab1",
        "brand": "MERCEDES"
      },
      "validationViolations": []
    }
  }
}

virtual

virtual?:boolean
ValueShortcutDescription
false(default)attribute is persisted in datastore
trueattribute is not persisted in datastore

Usually any attribute will be persisted in the datastore when saved by a create or update mutation. You could decide that an entity item should have an attribute value that is not simply a value from the datastore but should be resolved otherwise. If an attribute has virtual: true - this attribute will only be included in the entity type and not the input types, filter or sort type.

These are the possible ways to resolve a virtual attribute:

  • resolve function of the attribute itself
  • afterTypeQuery and afterTypesQuery hook

Virtual Attribute Example

Let's assume we know the year of manufacturing of a car and want to provide the age of the car as part of the car item. The "age" attribute should not provided by an API client, nor stored in the datastore but calculated every time a car item is delivered for a query.

Since "age" is a virtual attribute is not a field in the input, filter or sort types.

{ 
  entity: {
    Car: {
      attributes: {
        brand: 'String!',        
        manufacturedYear: 'Int!',
        age: {
          type: 'Int!',
          virtual: true,
          resolve: ({item}) => new Date().getFullYear() - item.manufacturedYear
        }
      }
    }
  }
}
Request Response
mutation createCar { 
  createCar( car: { 
    	brand: "Mercedes" manufacturedYear: 2015  } 
  	){
    car{ id brand manufacturedYear age }
    validationViolations 
  }
}
{
  "data": {
    "createCar": {
      "car": {
        "id": "5fcf89af35c4651db00e2fba",
        "brand": "Mercedes",        
        "manufacturedYear": 2015,
        "age": 5
      },
      "validationViolations": []
    }
  }
}