Domain configuration
Operations
Attributes configuration

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 valueDescription
stringshortcut for type of the attribute
falseexclude an (entity) attribute from the input
OperationAttributeConfigconfiguration 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 valueTypeDefaultDescription
typestring | string[]from entity attributefield type
listbooleanfrom entity attribute or falsewhether single or list of values
requirednumber | string | ConfigSource<boolean>from entity attribute or falserequired value in input
uniquestring | boolean | (string[])from entity attributeunique value for entity attribute
patternConfigSource<string>from entity attributestring value should match this pattern
allowedConfigSource<unknown[]>from entity attributeallowed values
rangeConfigSource<OperationRange>from entity attributeallowed value range
cardinalityConfigSource<OperationRange>allowed length of list values
validationValidateJs | ValidationConfigSourcefrom entity attributeattribute validation
defaultValueConfigSource<any>value of attribute when not in in input
valueConfigSource<any>value of attribute
shadowbooleanfalseattribute not in input
omitbooleanfalsenot value for this attribute allowed in input
dispositionConfigSource<any>default implementationcustom attribute disposition
resourcesConfigSource<OperationAttributeResources>
descriptionstringdescription operation mutation in schema

type

{ type: string | string[] }

Config valueDefaultDescription
stringwhen 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 power from Int to Float
  • 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: Car

Therefore 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 valueDescription
undefinedsame as false
falsethe value is not a list
truethe value is a list
ShortcutResolved 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: Car
input RentCarInputCar {
  accessories: [String]
  id: ID
  brand: [String]
  power: Int
}

required

{ required: undefined | number | string | ConfigSource<true | false> }

Config valueDescription
undefinedsame as { required: false }
falsethe attribute does not require a value in the input
trueattribute does require a value in the input; a missing value will add a ValidationViolation
stringsame as { required: { expression: 'string' } }
numbersame as { required: { cardinality: { min: number } } }
ShortcutResolved 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 valueDescription
undefinedsame as false
falseno validation added
trueadd a validation that the value of this attribute must be unique for the entity of the input configuration
stringadd 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 valueDescription
undefinedno additional validation
stringvalidation 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 valueDescription
undefinedno 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 valueDescription
undefinedno additional validation
OperationRangevalidation 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 valueDescription
undefinedno additional validation
numbersame as cardinality: { min: number }
OperationRangevalidation 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: 3
mutation 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 valueDescription
undefinedsame as true
trueno additional validation
falseadds a generic validation violation for this attribute
stringadds the string as validation violation for this attribute
ValidateJsattribute 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: 20
mutation{
  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 valueDescription
undefinedno default value
unknownthe 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
mutationresponse
``` 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 valueDescription
undefinedvalue comes from operation request
unknownthe 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: Car
mutation RentCar { 
  RentCar( car: { brand: "Porsche"  } ) {  result { car { brand } } }
}
{ car: { brand: 'PORSCHE' } }

shadow

{ shadow: undefined | false | true }

Config valueDescription
undefinedno additional validation
falseregular attribute
truethis 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: 18
input 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 valueDescription
undefinedsame as true
truea value for this attribute is allowed in the input
falsea 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 valueDescription
undefinedno custom disposition
anyany 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
            - d
mutation { 
  RentCar( Car: { gender: m  } ) 
  { inputDispositions }
}
{
  'Car.brand': {
    type: 'String',
    list: false,
    required: false,
    omit: false,
    custom: [ 'Mercedes', 'BMW' ]
  }
}

resources

{resources: undefined | ConfigSource<OperationAttributeResources> }

Config valueDescription
undefinedno resources
OperationAttributeResourcesresource values attribute
OperationAttributeResourcesDescription
labelthe label for a field in a UI
presomething to show before the label for a field in a UI
postsomething to show after the label for a field in a UI
hintsome hint for a field in a UI
extrasanything

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
}