Domain configuration
Operations
Input configuration

Operation Input

Usually the most complex part of an operation configuration is the input configuration. Here you define how the mutation or API call appears to a client and how it's input is validated.

{ 
  [OperationName]: { 
    input: string | OperationInputConfig | {[name:string]: true | OperationInputConfig } 
  } 
}
Config valueDescription
stringshortcut uses the string value as Entity name;
same as:
{ input: { EntityName: { entity: EntitName } } }
OperationInputConfigshortcut uses the name of the Operation as typename of the input type;
same as:
{ input: { OperationName: OperationInputConfig } }
{ [InputName]: true }shortcut uses the InputName value as Entity name;
same as:
{ input: { InputName: { entity: InputName } } }
{ [InputName]: OperationInputConfig }configuration of operation input

Entity as Input

The simplest operation input configuration would be referencing an entity

entity:
  Car:
    attributes:
      brand: String!
      licence: String!
operation:
  RentCar:
    input: Car
    result: Car

This example would take a "Car" input, saves it and returns the created entity item. In other words it behaves just like the createCar and updateCar mutation. While this might seem without value, you have now a much broader range of possibilities to express business and validation logic.

The notation above is in fact a shortcut to this notation, which expresses the same:

entity:
  Car:
    attributes:
      brand: String!
      licence: String!
operation:
  RentCar:
    input: 
      Car: 
        entity: Car
    result: Car

As you might guess you could name the input configuration as you like, but only when you name it exactly like the entity ActiveQL is able to automagically save the entity and to return the saved entity item. Read more at OperationInputConfig.entity

Referencing Entities in Input

When dealing with entities in an input configuration you decide whether you want to include the attributes of the entity (with the possibilities to create and/or update an entity item) or just the ID to reference to an existing entity item.

In the following example the Car entity as an assocTo relationship to the Driver entity. Since we did not nest the Driver in the input (see: assoc) the input type will include a driverId field.

entity:
  Car:
    assocTo: Driver
    attributes:
      brand: String!
      licence: String
  Driver:
    attributes:
      firstname: String
      lastname: String!
operation:
  RentCar:
    input: Car
input RentCarInputCar {
  id: ID
  brand: String
  licence: String
  driverId: ID
}

ActiveQL will validate this field to have an existing ID of the Driver entity.

OperationInputConfig

Usually you want to define more complex or dedicated input for an operation.

{ [InputName:string]: OperationInputConfig } }

Config ValueTypeDefaultDescription
entitystring | trueInput based on an entity
fieldNamestringname of the OperationInputConfig in the configuration mapthe name of the field in the input type
inputTypeNamestringgeneratedthe name of the schema input type
listbooleanfalse
requiredstring | number | ConfigSource<boolean>false
cardinalityConfigSource<number | OperationRange>validates min and max occurrences of input values
attributestrue |
{[name:string]:string | boolean | OperationAttributeConfig}
truethis inputs attributes / fields of the generated schema input type
assoc{[name:string]:OperationInputConfig}association (nesting) with other operation input types
validationValidationConfigSource | ValidationConfigSource[]
refundefined | truewhether this input allows for using data from a referenced path
descriptionstringdescription of the operation's mutation in the schema

entity

{ entity: undefined | true | false | string }

Config ValueDescription
undefinedimplicit input based on the entity with the name of this input configuration if and only if an entity with such name exists
trueexplicit input based on the entity with the name of the input (if exists)
falseinput is not based on an entity even when one with the name of the input exists
stringInput based on this entity (if exists)

The example from above, when we have an input based on an entity could have been written also as:

entity:
  Car:
    attributes:
      brand: String!
      licence: String!
operation:
  RentCar:
    input: 
      Car: 
        entity: Car
    result: Car

In this example the GraphQL schema mutation type would look like this:

'RentCar(car: RentCarInputCar): RentCarReturn'

As you might see we could have chosen any key for the OperationInputConfig. But only when we use the name of the entity we get some "automagic" result that contains the saved entity item without us configuring anything.

Having an input based on an entity would:

  • build the input type based on the attributes of the entity (except configured otherwise)
  • validate the input of the operation based on the attributes of the attributes
  • save the input values as an entity item when the validation passes

fieldName

{ fieldName: string | undefined }

Config valueDescription
undefinedentity name or name in configuration map
stringoverriding name of the field in the schema input type

Per default the fieldName of the mutation input type of the example above would be "car" since it is the name of the InputConfig in the configuration. Only set this when knowing exactly what you want to achieve.

'RentCar(car: RentCarInputCar): RentCarReturn'

You could change the fieldName to 'automobile' like so:

entity:
  Car:
    attributes:
      brand: String!
      licence: String!
operation:
  RentCar:
    input: 
      Car: 
        entity: Car
        fieldName: automobile
    result: Car

In the following example the GraphQL schema mutation type would look like this:

'RentCar(automobile: RentCarInputCar): RentCarReturn'

inputTypeName

{ inputTypeName: undefined | string }

Config valueDescription
undefined{OperationName}Input{InputConfigName}
stringoverride the name of the schema input type

The name of the input type in the GraphQL schema. Only set this when knowing exactly what you want to achieve.

'RentCar(Car: RentCarInputCar): RentCarReturn'

You can change the typename to something different:

entity:
  Car:
    attributes:
      brand: String!
      licence: String!
operation:
  RentCar:
    input: 
      Car: 
        entity: Car
        inputTypeName: SomeOtherTypeName
    result: Car
'RentCar(car: SomeOtherTypeName): RentCarReturn'

list

{ list: undefined | true | false }

Config valueDescription
undefinedsame as false
truethe input type is a list type
falsethe input type is not a list type

Set to true when the input should accept a list of values:

entity:
  Car:
    attributes:
      brand: String!
      licence: String!
operation:
  RentCar:
    input: 
      Car:         
        list: true
    result: Car

The operation mutation input would then be a list of RentCarInputCar types. Please note that the result is still a single Car type. We will cover how to configure complex operation results.

'RentCar(cars: [RentCarInputCar]): RentCarReturn'

required

{ required: undefined | string | number | ConfigSource<boolean> }

Config valueDescription
undefinedsame as false
ConfigSource<boolean>ConfigSource whether the input type is required
stringshortcut to FEEL expression to determine whether the input is required
numbershortcut for cardinality

The input type will not become required in the schema but is validated and shows its actual value in the InputDispositions.

ConfigSource<boolean>

entity:
  Car:
    attributes:
      brand: String!
      licence: String!
operation:
  RentCar:
    input: 
      Car:         
        required: "true"
    result: Car

Having a required input and not providing a value:

mutation{ RentCar( car: null ) { validationViolations  } }

would result in a ValidationViolation

[ 
  { path: 'car', message: 'is required' } 
]

string

This shortcut ... resolves to
```yaml input: InputName: required: string value ``` ```yaml input: InputName: required: expression: string value ```

number

A number value is a shortcut and resolved to a cardinality

This shortcut ... resolves to
```yaml input: InputName: required: 3 ``` ```yaml input: InputName: required: true cardinality: min: 3 ```

cardinality

{ cardinality: undefined | ConfigSource<number|OperationRange> }

Config valueDescription
undefinedno validation on the number of items in the input
ConfigSource<number>validates min occurrences of input values
ConfigSource<OperationRange>validates min and max occurrences of input values

The cardinality validates the occurrences of a list of input types in an operation's input. Using it with a non-list input would have no further effect and you will get a message in the RuntimeLog

{ 
  "message": "range ignored since it's not a list", 
  "path": "OperationDispositionResolver.Operation.RentCar",  
  "severity": "WARN"
}

Assuming you have a list of inputs cardinality will validate accordingly:

entity:
  Car:
    attributes:
      brand: String!
      licence: String!
operation:
  RentCar:
    input: 
      Car: 
        entity: Car
        list: true
        cardinality: 
          min: 2
          max: 3
    result: Car

When trying to call this operation with just one car input

mutation {
  RentCar( cars: [{ brand: "BMW", licence: "M-WI-2023" }] ){ validationViolations }
}

no item will be saved, instead the result will include a ValidationViolation

{ "path": "cars", "message": "should be at least 2 length but is 1" }

attributes

{ attributes: undefined | true | false | OperationAttributesConfig }

Config valueDescription
undefinedsame as true
truewhen the input configuration is based on an entity, all entity attributes
falsedon't add any entity attributes except the ID
OperationAttributesConfigplease refer to the documentation for Operation Attribute Configuration

When an input configuration is based on an entity the input type gets all attributes of this entity

entity:
  Car:
    attributes:
      brand: String
      licence: String!
  Driver: 
    attributes: 
      firstname: String!
      lastname: String!
  Accessory: 
    attributes: 
      name: String
      price: Float.2
  Insurance: 
    attributes: 
      insuranceType: 
        - liablity
        - hull
      coverage: Float.2
operation:
  RentCar:
    input: 
      Car: true
      Driver: 
        attributes: true
      Accessory: 
        list: true
        attributes: false
      Insurance:
        attributes: 
          id: false
    result: Car

assoc

{ assoc: {[assocName]: OperationInputConfig } }

Config valueDescription
{ assocName: OperationInputConfig}nest another operation input in this operation input

In an operation you can nest entities as input. While you can also nest attributes attribute types you can use associations of entities to benefit from automagic saving of these associations. Especially this aggregation of entities in combination with dynamic attribute configuration makes the strength of operations and allow the simple creation of business specific API calls based on your business domain configuration.

In the following example we have two entities with an assocTo relationship from Car to Driver. In the RentCar operation we want the input to be a car input. Inside the car input we nest (assoc) a driver input. We will see later that we can this also the other way around. Please note that we do not specify the association between Car and Driver here. This type of association is already defined by the entities.

entity:
  Driver: 
    assocFrom: Car
    attributes: 
      firstname: String
      lastname: String!
  Car:
    assocTo: Driver
    attributes:
      brand: String
      licence: String!
operation:
  RentCar:
    input: 
      Car:
        assoc: Driver
    result: Car

When calling this operation mutation

mutation { 
  RentCar( 
    car: { 
      brand: "BMW", licence: "M-WI-2023" 
      driver: { firstname: "Thomas", lastname: "Thompson"} 
  }){
    validationViolations 
    result { car { id brand driver { id firstname lastname } } }
  }
} 

We get the expected entity items

{
  car: { 
    id: "100000001", brand: "BMW",
    driver: { id: "100000002", firstname: "Thomas", lastname: 'Thompson' }
  }
}

How you combine entities in an operation input however can be dedicated according to the business domain. Let's assume for this simple example we want the API call to include the driver with the car nested inside, we would write:

entity:
  Driver: 
    assocFrom: Car
    attributes: 
      firstname: String
      lastname: String!
  Car:
    assocTo: Driver
    attributes:
      brand: String
      licence: String!
operation:
  RentCar:
    input: 
      Driver:
        assoc: Car
    result: Driver

The mutation would now look like something like this:

mutation { 
  RentCar( 
    driver: {
      firstname: "Thomas" lastname: "Thompson"
      cars: [{ brand: "BMW" licence: "M-WI-2023"}]
  }) {
    validationViolations 
    result {
      driver { id firstname lastname
        cars { id brand licence }
      }
    }
  }
}

Since we configured the Driver to be the result of the operation we'll get this result:

{
  driver: { 
    id: "100000003", firstname: "Thomas", lastname: 'Thompson' ,
    cars: [{ id: "100000004", brand: "BMW" }]
  }
}

validation

{ validation: ValidationConfigSource | ValidationConfigSource[] }

type ValidationConfigSource = ConfigSource<void|boolean|string|(boolean|string)[]>

ConfigSource resultDescription
undefined | trueif a validation returns true or undefined the validation passed
falsea generic validationViolation is added to the result
stringif a validation returns a string a validationViolation with this message is added to the result
(boolean | string)[]>a validation may return a list of boolean or string values; anything but undefined or true will be added as validationViolation to the result

You can add validation logic to an input configuration. In your ConfigSource you have access to the whole operation though, so deciding what to validate where is mostly driven by what the path of the validationViolation should be. If there are many independent validations you can have a list of ValidationConfigSource at the input configuration.

In this example we validate the car input with a decision table. Please note how we use the hitPolicy Collect so the decision table does not return just the first rule that matches the conditions but gives us the output of every matching rule as a list.

We could have named the output column differently as long as there is only one output. But it is recommended to name it validation in order to get a more descriptive message in the ValidationViolation when the output is not a string but a boolean.

We also validate the age of the driver to be over 21. We could have also added it to the "birthdate" attribute though.

entity:
  Driver: 
    assocFrom: Car
    attributes: 
      firstname: String
      lastname: String!
      birthdate: Date!
  Car:
    assocTo: Driver
    attributes:
      brand: String!      
      power: Int
      color: String
operation:
  RentCar:
    input: 
      Car:
        validation:
          hitPolicy: Collect
          input:  ["car.brand", "car.power",  "car.color"                             ]
          output: [                                         "validation"              ]
          rules:
            -     ["'Tesla'",   "-",          "-",          "false"                   ] 
            -     ["-",         "-",          "'pink'",     "'no pink cars, please'"  ] 
            -     ["-",         "<= 100",     "-",          "false"                   ] 
            -     ["-",         "-",          "-",          "null"                    ] 
        assoc: 
          Driver:
            entity: Driver
            validation:
              expression: age("@driver.birthdate") >= 21
    result: Car
now: 2023-08-08T09:00:00

When calling this operation with invalid data ...

mutation { 
  RentCar( 
    car: { 
      brand: "Porsche" color: "pink" power: 100
      driver: { firstname: "Thomas" lastname: "Thompson" birthdate: "2005-04-09" } 
  }){
    validationViolations 
    result { car { id brand driver { id firstname lastname } } }
  }
} 

... we get the expected validation violations.

[
  {
    "path": "car",
    "message": "no pink cars, please"
  }, 
  {
    "path": "car",
    "message": "decision table rule 3 failed", 
  }, 
  {
    "path": "car.driver",
    "message": 'did not satisfy expression: age("@driver.birthdate") >= 21'
  }
]

ref

{ ref: undefined | true }

If set to true a field with the name ref is added to input type. An API client can provide a path to any point in the input to copy data from. This can become handy when you have similar data on more than one location.

description

{ description: string }

The description becomes part of the GraphQL schema

entity:
  Car:
    attributes:
      brand: String!
      licence: String
operation:
  RentCar:
    input: 
      Car:         
        required: true
        description: >
          use this input to request a car rent for a specific brand, 
          optionally a certain car by providing its licence number.
    result: Car 
"""
use this input to request a car rent for a specific brand,  optionally a certain car by providing its licence number.
"""
input RentCarInputCar {
  id: ID
  brand: String
  licence: String
}