Operation Attributes
Besides relying on entity attributes you can override some properties of an entity attribute or add any attribute to the input. If you do the latter you will usually also implement one or more of the lifecycle methods of the operation, since the automagic entity saving would not recognice these attributes.
{ attributes: {[attributeName]: string | false | OperationAttributeConfig } }
| Config value | Description |
|---|---|
string | shortcut for type of the attribute |
false | exclude an (entity) attribute from the input |
OperationAttributeConfig | configuration of the attribute |
Type shortcut
Any string value for an attributeName is a shortcut for the type of the attribute.
| So this | ... resolves to |
| ```yaml attributes: power: Int ``` | ```yaml attributes: power: type: Int ``` |
Exclude entity attribute
When having entity attributes you might want to exclude some attributes from it. A usual scenario would be not to include the ID of the entity since yould not allow updates but only the creation of new entity items.
| When setting `id` to `false` | ... no `ID` field in the generated _schema input type_ |
| ```yaml from:test/operations/attributes:domainConfig1 entity: Car: attributes: brand: String licence: String! operation: RentCar: input: Car: entity: Car attributes: id: false result: Car ``` | ```json from:test/operations/attributes:schema1 input RentCarInputCar { brand: String licence: String } ``` |
OperationAttributeConfig
{ attributes: { [attributeName]: OperationAttributeConfig } }
| Config value | Type | Default | Description |
|---|---|---|---|
| type | string | string[] | from entity attribute | field type |
| list | boolean | from entity attribute or false | whether single or list of values |
| required | number | string | ConfigSource<boolean> | from entity attribute or false | required value in input |
| unique | string | boolean | (string[]) | from entity attribute | unique value for entity attribute |
| pattern | ConfigSource<string> | from entity attribute | string value should match this pattern |
| allowed | ConfigSource<unknown[]> | from entity attribute | allowed values |
| range | ConfigSource<OperationRange> | from entity attribute | allowed value range |
| cardinality | ConfigSource<OperationRange> | allowed length of list values | |
| validation | ValidateJs | ValidationConfigSource | from entity attribute | attribute validation |
| defaultValue | ConfigSource<any> | value of attribute when not in in input | |
| value | ConfigSource<any> | value of attribute | |
| shadow | boolean | false | attribute not in input |
| omit | boolean | false | not value for this attribute allowed in input |
| disposition | ConfigSource<any> | default implementation | custom attribute disposition |
| resources | ConfigSource<OperationAttributeResources> | ||
| description | string | description operation mutation in schema |
type
{ type: string | string[] }
| Config value | Default | Description |
|---|---|---|
string | when entity input configuration: type of the entity attribute otherwise JSON (with warning) | field type |
string[] | on-the-fly enum |
In the following example we add two attributes in addition to the entity attributes.
- We make use of the inline enum definition for
rentalType - try to change the type of the entity attribute for
powerfromInttoFloat - do not specify a type for
rentalDate
entity:
Car:
attributes:
brand: String
power: Int!
operation:
RentCar:
input:
Car:
entity: Car
attributes:
power: Float
rentalDate:
rentalType:
- shortterm
- medium
- longterm
result: CarTherefore we get some warnings in the log
[
{
"message": "can't change entity attribute type 'Int' to 'Float' ",
"path": "RentCarCarInput.power"
},
{
"message": 'has no type, using "JSON" for now, but you should change this',
"path": "RentCarCarInput.rentalDate"
}
]Nontheless we get a functioning input type and enum type.
input RentCarInputCar {
power: Int
rentalDate: JSON
rentalType: RentCarCarInputRentalTypeEnum
id: ID
brand: String
}enum RentCarCarInputRentalTypeEnum {
shortterm
medium
longterm
}Type ID vs String when referencing other entities
When ActiveQL adds a reference to an entity from an assocTo that is not an assoc of the input configuration itself, it uses the foreignKey as attribute name and the type ID. You can however also add references to existing entity items by using the foreignKey of that entity as attribute name anywhere else.
Everytime the type ID is used, ActiveQL looks up an entity with a foreignKey that matches the attribute name and if such an entity exists adds an entityId disposition to this attribute, meaning it will validate any value for this attribute to be an existing ID of the entity. If you want to ignore this requirement you can choose the type String for the entity id and handle its value in one of the operation's lifecycle methods yourself.
(Please note the same is true for list values when having an assocToMany relationship with a list field with the name matching the foreignKeys of the entity and the type [ID]. )
In this example
list
{ list: undefined | true | false }
| Config value | Description |
|---|---|
undefined | same as false |
false | the value is not a list |
true | the value is a list |
| Shortcut | Resolved to |
|---|---|
{ attributeName: Type[] } | same as { attributeName: { type: Type, list: true } } |
{ attributeName: Type![] } | same as { attributeName: { type: Type, list: true, required: true } } |
entity:
Car:
attributes:
brand: String![]
power: Int!
operation:
RentCar:
input:
Car:
entity: Car
attributes:
accessories:
type: String
list: true
result: Carinput RentCarInputCar {
accessories: [String]
id: ID
brand: [String]
power: Int
}required
{ required: undefined | number | string | ConfigSource<true | false> }
| Config value | Description |
|---|---|
undefined | same as { required: false } |
false | the attribute does not require a value in the input |
true | attribute does require a value in the input; a missing value will add a ValidationViolation |
string | same as { required: { expression: 'string' } } |
number | same as { required: { cardinality: { min: number } } } |
| Shortcut | Resolved to |
|---|---|
{ attributeName: Type! } | same as { attributeName: { type: Type, required: true } } |
Example
entity:
Car:
attributes:
brand: String[]!
licence: String
Driver:
attributes:
firstname: String
lastname: String!
operation:
RentCar:
input:
Rental:
assoc:
Car: true
attributes:
rentalDate: Date!
driverId: ID!mutation { RentCar { inputDispositions } }{
"Rental.rentalDate": {
"type": "Date",
"list": false,
"required": true,
},
"Rental.driverId": {
"type": "ID",
"list": false,
"required": true,
},
"Rental.car.brand": {
"type": "String",
"list": true,
"required": true,
},
}unique
{ unique: undefined | false | true | string | string[] }
| Config value | Description |
|---|---|
undefined | same as false |
false | no validation added |
true | add a validation that the value of this attribute must be unique for the entity of the input configuration |
string | add a validation that the value of this attribute must be unique for the entity of the input configuration with the scope of another attribute |
string[] | add a validation that the value of this attribute must be unique for the entity of the input configuration with the scope of another attributes |
This makes only sense when the attribute is part of an input configuration that is based on an entity. It adds a validation that the value of this attribute is unique with this entity.
pattern
{ pattern: undefined | ConfigSource<string> }
| Config value | Description |
|---|---|
undefined | no additional validation |
string | validation added for string values to match this pattern |
operation:
RentCar:
input:
car:
attributes:
licence:
type: String
pattern: ^[A-ZÖÜÄ]{1,3} [A-ZÖÜÄ]{1,2} [1-9]{1}[0-9]{1,3}$ mutation RentCar {
RentCar( car: { licence: "HH TRX 2023" }) {
validationViolations
}
}[{
"path": "car.licence",
"message": "value 'HH TRX 2023' does not match pattern '/^[A-ZÖÜÄ]{1,3} [A-ZÖÜÄ]{1,2} [1-9]{1}[0-9]{1,3}$/'"
}]allowed
{ allowed: undefined | ConfigSource<unknown[]> }
| Config value | Description |
|---|---|
undefined | no additional validation |
unknown[] | input value must be equal to one of this values |
In this example we evaluate a decision table to determine the allowed values. Note the use of the Collect hit policy to get a list of values instead of just the value from the first rule that meets the condition.
operation:
RentCar:
input:
car:
attributes:
brand: String!
vehicleClass:
type: String
allowed:
hitPolicy: Collect
input: [car.brand]
output: [vehicleClass]
rules:
- ['"Porsche", "BMW"', '"Convertible"']
- ['"BMW", "VW"', '"Sedan"']
- ['"BMW", "Mercedes"', '"SUV"']
- ['"Porsche"', '"Sportscar"']
- ['"BMW", "Mercedes"', '"Pickup"']mutation{
RentCar( car: { brand: "Porsche" vehicleClass: "SUV" }) {
inputDispositions
validationViolations
}
}[
{
"path": "car.vehicleClass",
"message": "value 'SUV' must be one of [\"Convertible\",\"Sportscar\"]"
}
]range
{ range: undefined | ConfigSource<OperationRange> }
| Config value | Description |
|---|---|
undefined | no additional validation |
OperationRange | validation for any number attribute value to be withing the range |
In this example we evaluate a decision table to determine the range values. Note the use of the output columns min and max. You need to name your output exactly like that to get a OperationRange. The inputDispositions and validationViolations show the found range values.
operation:
RentCar:
input:
car:
attributes:
brand: String!
power:
type: Int
range:
input: [car.brand]
output: [min, max]
rules:
- ['"Porsche"', 100, 300]
- ['"VW"', 80, 120]
- ['"BMW"', 90, 200]
- ['-', 10, 100]mutation{
RentCar( car: { brand: "VW" power: 130 }) {
validationViolations
}
}[{
"path": "car.power",
"message": "value '130' must not be greater than '120'"
}]cardinality
{ cardinality: undefined | number | ConfigSource<OperationRange> }
| Config value | Description |
|---|---|
undefined | no additional validation |
number | same as cardinality: { min: number } |
OperationRange | validation the length of the attribute value list for min and max length |
This configuration will only be considered for list attributes.
operation:
RentCar:
input:
Car:
attributes:
brand:
type: String[]
cardinality:
min: 1
max: 3mutation RentCar {
RentCar(Car: { brand: ["Volkswagen", "Porsche", "Mercedes", "BMW" ] }) {
validationViolations
}
}[{"message": "should be max of length 3 but is 4", "path": "Car.brand"}]validation
{ validation: undefined | ValidateJs | ConfigSource<undefined|boolean|string|(boolean|string)[] }
| Config value | Description |
|---|---|
undefined | same as true |
true | no additional validation |
false | adds a generic validation violation for this attribute |
string | adds the string as validation violation for this attribute |
ValidateJs | attribute validation according to ValidateJs (opens in a new tab) |
operation:
RentCar:
input:
Rental:
attributes:
brand:
type: String
validation:
expression: '@brand != "Tesla"'
power:
type: Int
validation:
numericality:
greaterThan: 20mutation{
RentCar( Rental: { brand: "Tesla" power: 20 }) {
validationViolations
}
}[{
"path": "Rental.brand",
"message": `did not satisfy expression: @brand != "Tesla"`
},
{
"path": "Rental.power",
"message": "must be greater than 20"
}]defaultValue
{ defaultValue: undefined | ConfigSource<unknown> }
| Config value | Description |
|---|---|
undefined | no default value |
unknown | the value of this attribute if no value is in the operation request |
In this example the default value of the color attribute (when none was send in the request) will be either "silver" when the brand is "Mercedes" or else "black".
entity:
Car:
attributes:
brand: String!
color:
- white
- silver
- black
operation:
RentCar:
input:
Car:
attributes:
color:
defaultValue:
expression: if at('brand') = "Mercedes" then "silver" else "black"
result: Car| mutation | response |
| ``` from:test/operations/attribute-default-value:mutation1 mutation { RentCar( car: { brand: "Porsche" } ) { result { car { brand color } } validationViolations } } ``` | ``` from:test/operations/attribute-default-value:result1 { car: { brand: 'Porsche', color: 'black' } } ``` |
| ``` from:test/operations/attribute-default-value:mutation2 mutation { RentCar( car: { brand: "Porsche" color: white } ) { result { car { brand color } } } } ``` | ``` from:test/operations/attribute-default-value:result2 { car: { brand: 'Porsche', color: 'white' } } ``` |
| ``` from:test/operations/attribute-default-value:mutation3 mutation { RentCar( car: { brand: "Mercedes" } ) { result { car { brand color } } } } ``` | ``` from:test/operations/attribute-default-value:result3 { car: { brand: 'Mercedes', color: 'silver' } } ``` |
value
{ value: undefined | ConfigSource<unknown> }
| Config value | Description |
|---|---|
undefined | value comes from operation request |
unknown | the value of this attribute |
Unlike defaultValue this ConfigSource does give the value for this attribute regardless of the presence of a value from mutation request.
Let's say you want a value always in upper case letters you could use a value expression to achieve this.
entity:
Car:
attributes:
brand: String!
operation:
RentCar:
input:
Car:
attributes:
brand:
value:
expression: upper( @brand )
result: Carmutation RentCar {
RentCar( car: { brand: "Porsche" } ) { result { car { brand } } }
}{ car: { brand: 'PORSCHE' } }shadow
{ shadow: undefined | false | true }
| Config value | Description |
|---|---|
undefined | no additional validation |
false | regular attribute |
true | this attribute is not part of the operations input interface but gets its value from somewhere else than the API request |
You can make a value available that can be treated as any other attribute value but is not part of the mutation request but rather based on other values.
See as in the following example we treat the age attribute as any other attribute, it is available for validation or any other business logic. But its value does not stem from the API request - it is not part of the input type - but in this case comes from a value expression. This is a common scenarion when you want to convert a value.
operation:
RentCar:
input:
Driver:
attributes:
firstname: String
lastname: String!
birthdate: Date!
age:
type: Int!
shadow: true
value:
expression: age( @birthdate )
validation:
numericality:
greaterThanOrEqualTo: 18input RentCarInputDriver {
firstname: String
lastname: String
birthdate: Date
}mutation RentCar {
RentCar(Driver: { lastname: "Smith" birthdate: "2010-10-20"}) {
validationViolations
}
}[{
path: 'Driver.age',
message: 'must be greater than or equal to 18'
}]omit
{ omit: undefined | ConfigSource<true | false> }
| Config value | Description |
|---|---|
undefined | same as true |
true | a value for this attribute is allowed in the input |
false | a value for this attribute is not allowed in the input |
Think of omit as the opposite of require. When it resolves to true a client is not allowed to send value in the mutation's request. While a shadow attribute is not part of the public interface of an operation, such an attribute is still visible for client but (in some cases) does not allow a value.
In the following example the power attribute requires a value, unless the brand is a Rolls-Royce in which case it prohibits any value (since as it is commonly known, a Rolls-Royce has always "sufficient" horsepower).
operation:
RentCar:
input:
Car:
attributes:
brand: String!
power:
type: Int
required:
expression: neq( "Rolls-Royce", "@brand" )
omit:
expression: Car.brand = "Rolls-Royce"mutation {
RentCar( Car: { brand: "Porsche" } ) { validationViolations }
}[{
"path": "Car.power",
"message": "is required"
}]mutation {
RentCar( Car: { brand: "Rolls-Royce" power: 200 } ) { validationViolations }
}[{
"path": "Car.power",
"message": "should be omitted and must not be part of input"
}]disposition
{ disposition?: ConfigSource<unknown> }
| Config value | Description |
|---|---|
undefined | no custom disposition |
any | any value as custom disposition for this attribute |
You can add additional values to the attributes input disposition. This is usually the case when you want to keep some logic for a client in your DomainGraph.
Let's say there is client that renders a UI for the entry of gender and car brand for a rental. It uses dropboxes for that. You might want to keep some brands on top in the dropdown list. The business logic for this should not be on the server, so we add a disposition to inform the client about which brands (here based on gender) it use for this.
operation:
RentCar:
input:
Car:
attributes:
brand:
type: String
disposition:
input: Car.gender
output: disposition
rules:
- [ '"m"', '["Mercedes", "BMW"]' ]
- [ '"f"', '["Porsche", "Smart", "Tesla"]' ]
- [ '-', '[]' ]
gender:
- m
- f
- dmutation {
RentCar( Car: { gender: m } )
{ inputDispositions }
}{
'Car.brand': {
type: 'String',
list: false,
required: false,
omit: false,
custom: [ 'Mercedes', 'BMW' ]
}
}resources
{resources: undefined | ConfigSource<OperationAttributeResources> }
| Config value | Description |
|---|---|
undefined | no resources |
OperationAttributeResources | resource values attribute |
OperationAttributeResources | Description |
|---|---|
label | the label for a field in a UI |
pre | something to show before the label for a field in a UI |
post | something to show after the label for a field in a UI |
hint | some hint for a field in a UI |
extras | anything |
You might want to hold some resources a client should use to present the attribute in a UI in the DomainGraph. You can add this to the input disposition.
operation:
RentCar:
input:
Car:
attributes:
brand:
type: String
resources:
input: [locale, Car.brand]
output: [label, hint ]
rules:
- ['"en"', '"Porsche"', '"Brand"', '"must be 21"' ]
- ['"en"', '-', '"Brand"', ]
- ['"de"', '"Porsche"', '"Marke"', '"mind. 21 Jahre"' ]
- ['"en"', '-', '"Marke"' ]mutation {
RentCar( Car: { brand: "Porsche" } )
{ inputDispositions }
}{
'Car.brand': {
type: 'String',
list: false,
required: false,
omit: false,
resources: {
label: 'Brand',
hint: 'must be 21'
}
}
}description
{ description?: string }
You can add a text to the field definition in the GraphQL schema which becomes part of your API documentation.
operation:
RentCar:
input:
Car:
attributes:
brand:
type: String!
description: >
This can be any vehicle brand, since no
further business logic is applied to it. input RentCarInputCar {
\"\"\"
This can be any vehicle brand, since no further business logic is applied to it.
\"\"\"
brand: String
}