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 value | Description |
|---|---|
string | shortcut uses the string value as Entity name; same as: { input: { EntityName: { entity: EntitName } } } |
OperationInputConfig | shortcut 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: CarThis 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: CarAs 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: Carinput 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 Value | Type | Default | Description |
|---|---|---|---|
| entity | string | true | Input based on an entity | |
| fieldName | string | name of the OperationInputConfig in the configuration map | the name of the field in the input type |
| inputTypeName | string | generated | the name of the schema input type |
| list | boolean | false | |
| required | string | number | ConfigSource<boolean> | false | |
| cardinality | ConfigSource<number | OperationRange> | validates min and max occurrences of input values | |
| attributes | true | {[name:string]:string | boolean | OperationAttributeConfig} | true | this inputs attributes / fields of the generated schema input type |
| assoc | {[name:string]:OperationInputConfig} | association (nesting) with other operation input types | |
| validation | ValidationConfigSource | ValidationConfigSource[] | ||
| ref | undefined | true | whether this input allows for using data from a referenced path | |
| description | string | description of the operation's mutation in the schema |
entity
{ entity: undefined | true | false | string }
| Config Value | Description |
|---|---|
undefined | implicit input based on the entity with the name of this input configuration if and only if an entity with such name exists |
true | explicit input based on the entity with the name of the input (if exists) |
false | input is not based on an entity even when one with the name of the input exists |
string | Input 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: CarIn 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 value | Description |
|---|---|
undefined | entity name or name in configuration map |
string | overriding 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: CarIn the following example the GraphQL schema mutation type would look like this:
'RentCar(automobile: RentCarInputCar): RentCarReturn'inputTypeName
{ inputTypeName: undefined | string }
| Config value | Description |
|---|---|
undefined | {OperationName}Input{InputConfigName} |
string | override 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 value | Description |
|---|---|
undefined | same as false |
true | the input type is a list type |
false | the 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: CarThe 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 value | Description |
|---|---|
undefined | same as false |
ConfigSource<boolean> | ConfigSource whether the input type is required |
string | shortcut to FEEL expression to determine whether the input is required |
number | shortcut 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: CarHaving 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 value | Description |
|---|---|
undefined | no 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: CarWhen 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 value | Description |
|---|---|
undefined | same as true |
true | when the input configuration is based on an entity, all entity attributes |
false | don't add any entity attributes except the ID |
OperationAttributesConfig | please 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: Carassoc
{ assoc: {[assocName]: OperationInputConfig } }
| Config value | Description |
|---|---|
{ 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: CarWhen 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: DriverThe 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 result | Description |
|---|---|
undefined | true | if a validation returns true or undefined the validation passed |
false | a generic validationViolation is added to the result |
string | if 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:00When 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
}