Entity Configuration
A business domain is mainly described by its entities and their relation to each other. Think of an entity as any thing in your business domain. We highly recommend using a Domain Driven Design (opens in a new tab) approach.
| option | description |
|---|---|
attributes | the attributes of the entity |
description | description to add to the schema documentation |
assocTo | has-one relationship to another entity |
assocToMany | has-many relationship with references to the other entity at this entity |
assocFrom | has-many as relationship to another entity as opposite site of a has-one or has-many relationship with references at the other entity |
assocBy | has-one as relationship to another entity as opposite site of a has-one relationship with references at the other entity |
seeds | generating seed or test data |
validation | callback for complex or non-attribute based validation |
permissions | role-based rights permission configuration |
union | defines a a union relationship between entities |
interface | defines a common interface other entities can implement |
implements | defines the implementation of an interface entity |
subscriptions | add subscriptions for create, update, delete of items |
onInit | callback on initialization of the entity |
typeOnly | if true skip all queries and resolvers and only add type to the schema |
typeQuery | skip or replace default type query resolver with custom resolver |
typesQuery | skip or replace default types query resolver with custom resolver |
createMutation | skip or replace default create mutation resolver with custom resolver |
updateMutation | skip or replace default update mutation resolver with custom resolver |
deleteMutation | skip or replace default delete mutation resolver with custom resolver |
statsQuery | skip or replace default entity statistics query resolver with custom resolver |
typeName | the name of the entity object type |
plural | the plural of the entity, used for generated queries and others |
singular | the singular of the entity, used for generated queries and others |
collection | the name of the collection (e.g. table) in the datastore for this entity |
path | the name of the path in a UI (e.g. ActiveQL Admin UI) for this entity |
ui | UI configuration - only relevant for the ActiveQL AdminUI |
foreignKey | the name to use as foreignKey for has-one relationships for this entity |
foreignKeys | the name to use as foreignKey for has-many relationships for this entity |
createInputTypeName | the name of the GraphQL input type for the create mutation |
updateInputTypeName | the name of the GraphQL input type for the update mutation |
filterTypeName | the name of the GraphQL input type for the filter of the types query |
sorterEnumName | the name of the GraphQL enum for sorting of the types query |
typeField | the name of the GraphQL field when this entity is part of a polymorphic relationship |
typesEnumName | the name of the GraphQL enum for all types that are union types of this entity or implement this entity as interface |
createMutationName | the name of the create mutation for this entity |
updateMutationName | the name of the update mutation for this entity |
deleteMutationName | the name of the delete mutation for this entity |
mutationResultName | the name of the GraphQL type that is returned for the create and update mutation |
typeQueryName | the name of the type query for this entity |
typesQueryName | the name of the types query for this entity |
statsQueryName | the name of the stats query for this entity |
We suggest writing the entity configuration in YAML files and simply includes these files in your Domain Configuration.
From the definition of an entity a lot of GraphQL schema types, queries and mutations are generated, incl. resolver to read and write its items to and from a datastore. The behavior is strongly opinionated and uses mostly conventions; nonetheless you can configure most of the details. You will probably only ever use the first few of these configuration options, unless you know very well what do you want to achieve:
Properties of Entity configuration
attributes
attributes?:{[name:string]:string|AttributeConfig}The attributes of an entity as key/value pairs. See Attribute Configuration for details.
entity:
Car:
attributes:
brand: string!
mileage: intdescription
description?:stringYou can add a description or documentation to your entity. This will become part of the schema and the public API.
entity:
Car:
description: >
A simple version of a car entity, purely for demonstration purposes.
We will try to demonstrate as much ActiveQL features as possible.
If you like ActiveQL - feel free to contribute more examples at
https://github.com/betterobjects/activeqlinterface
| Type | Default | Description |
|---|---|---|
boolean | false | If true creates the entity's type as an GraphQL interface |
As a convenience feature you can define attributes in an interface entity that all entities that implement this interface will inherit.
entity:
Car:
interface: true
attributes:
brand: string!
Convertible:
implements: Car
attributes:
softtop: boolean
Sedan:
implements: Car
attributes:
doors: intimplements
| Type | Default | Description |
|---|---|---|
string | string[] | Configures the entity to implement all interfaces from this entity names |
entity:
Car:
interface: true
attributes:
brand: string!
Convertible:
implements: Car
attributes:
softtop: boolean
Sedan:
implements: Car
attributes:
doors: intSchema (excerpt)
interface Car {
id: ID!
brand: String!
createdAt: DateTime!
updatedAt: DateTime!
}
type Convertible implements Car {
id: ID!
softtop: Boolean
createdAt: DateTime!
updatedAt: DateTime!
brand: String!
}
type Mutation {
deleteCar(id: ID): [String]
createConvertible(
convertible: ConvertibleCreateInput
): SaveConvertibleMutationResult
updateConvertible(
convertible: ConvertibleUpdateInput
): SaveConvertibleMutationResult
deleteConvertible(id: ID): [String]
createSedan(sedan: SedanCreateInput): SaveSedanMutationResult
updateSedan(sedan: SedanUpdateInput): SaveSedanMutationResult
deleteSedan(id: ID): [String]
}
type Query {
cars(filter: CarFilter, sort: CarSort): [Car]
convertible(id: ID): Convertible
convertibles(filter: ConvertibleFilter, sort: ConvertibleSort): [Convertible]
sedan(id: ID): Sedan
sedans(filter: SedanFilter, sort: SedanSort): [Sedan]
}
type Sedan implements Car {
id: ID!
doors: Int
createdAt: DateTime!
updatedAt: DateTime!
brand: String!
}Request
mutation addCars {
createSedan( sedan: { brand: "VOLKSWAGEN" doors: 4 } )
createConvertible( convertible: { brand: "PORSCHE", softtop: true } )
}query listCars {
cars {
... on Convertible {
softtop
}
... on Sedan {
doors
}
brand
__typename
}
}Result
{
"data": {
"cars": [
{
"softtop": true,
"brand": "PORSCHE",
"__typename": "Convertible"
},
{
"doors": 4,
"brand": "VOLKSWAGEN",
"__typename": "Sedan"
},
{
"softtop": null,
"brand": "PORSCHE",
"__typename": "Convertible"
},
{
"doors": 4,
"brand": "VOLKSWAGEN",
"__typename": "Sedan"
}
]
}
}union
| Type | Default | Description |
|---|---|---|
string[] | Configures the entity's type to be the union of 2 or more entity types |
entity:
Convertible:
attributes:
brand: string
softtop: boolean
Sedan:
attributes:
brand: string
doors: int
Car:
union:
- Convertible
- SedanSchema (excerpt)
union Car = Convertible | Sedan
type Convertible {
id: ID!
brand: String
softtop: Boolean
createdAt: DateTime!
updatedAt: DateTime!
}
type Sedan {
id: ID!
brand: String
doors: Int
createdAt: DateTime!
updatedAt: DateTime!
}GraphQL
mutation addCars {
createSedan( sedan: { brand: "VOLKSWAGEN", doors: 4 } ){ sedan: { id } }
createConvertible( convertible: { brand: "PORSCHE", softtop: true }){ convertible: { id } } }
}
query listCars {
cars {
... on Convertible {
brand
softtop
}
... on Sedan {
brand
doors
}
__typename
}
}Result
{
"data": {
"cars": [
{
"brand": "PORSCHE",
"softtop": true,
"__typename": "Convertible"
},
{
"brand": "VOLKSWAGEN",
"doors": 4,
"__typename": "Sedan"
}
]
}
}seeds
Sometime you want to define some seed data to your application, you can easily initialize the DataStore with. You can configure this in the seeds section.
For more details e.g. how to reference type items see Seed Configuration.
entity:
Car:
attributes:
brand: string!
license: string!
mileage: int
seeds:
- items:
- brand: VOLKSWAGEN
license: AA 1234
mileage: 10000
- brand: Mercedes
license: BB 2345
mileage: 20000
- brand: PORSCHE
license: CC 3456
mileage: 30000
- environment: test
items:
100:
brand:
faker: vehicle.manufacturer
licence:
rfs: alphaUpper(1, 3).append(['-', ' ']).alphaUpper( 2, 3 ).append(' ').numeric( 3, 4 )
mileage:
random:
min: 10000
max: 120000validation
Often validation takes place on the attribute level. Check Attribute Validation for details.
But sometimes you want to add validation to the entity itself, when its logic is not bound to an attribute, e.g. if it reflects two or more attributes - in this case you can add a function that is called every time before the entity item is created or updated.
validation?:( item:any, runtime:Runtime ) => ValidationReturnTypeThe ValidationReturnType is usually of type ValidationViolation or a string.
ValidationViolation
| Property | Description |
|---|---|
| path (optional) | should point to the path in the result data where the ValidationViolation occurred |
| message | a descriptive message |
ValidationReturnType
The ValidationReturnType can be
| Type | Description |
|---|---|
undefined | signals the validation passes |
ValidationViolation | will be added to the list of other ValidationViolation and sent to the client |
ValidationViolation[] | all will be added to the list of other ValidationViolation and sent to the client |
string | will be handled as ValidationViolation with this string as message and no path |
Promise<...> | any of the types above can also be returned as Promise |
Your custom method will get the item that is about to be saved. Please note that for updates the item is complete, that means even if a client only attempts to update one attribute, all other existing attribute values are set prior the call to this method.
If a validate method is present and returns something else but undefined or [] the validation fails and the entity item will not be saved. The return message (probably combined with other ValidationViolation
messages) will be send to the client.
For more information please check Validation.
Example
export const example3:DomainConfiguration = {
entity:{
Car: {
attributes: {
brand: 'string!',
mileage: 'int'
},
validation: (item:any) => (item.brand !== 'Mercedes' && item.mileage > 300000 ) ?
'I wouldnt believe that' : undefined
}
}
}Request
mutation {
createCar(
car: { brand: "Kia", mileage: 310000 }
)
{
car{ id brand mileage }
validationViolations
}
}Response
{
"data": {
"createCar": {
"car": null,
"validationViolations": [
{
"path": null,
"message": "I wouldnt believe that"
}
]
}
}
}Request
mutation {
createCar(
car: { brand: "Mercedes", mileage: 310000 }
)
{
car{ id brand mileage }
validationViolations
}
}Response
{
"data": {
"createCar": {
"car": {
"id": "5fa87cc40dd707984e939524",
"brand": "Mercedes",
"mileage": 310000
},
"validationViolations": []
}
}
}permissions
Role based declaration of permissions to access this entity's
entity: {
Car: {
attributes: {
brand: 'String!',
mileage: 'Int!',
},
permissions: {
admin: true,
manager: { read: true, create: true, update: true },
assistant: ({action}) => action === CRUD.READ ? { brand: { $ne: 'Ferrari'} } : false
}
}
}You can allow/disallow access to any query and mutation (via CRUD actions) based on the role(s) of a Principal and even filter allowed entity items via expressions. For details see Permissions.
subscriptions
Per default any lifecycle action is published as an EntityLifecycle event at the Runtime. You can disable this by setting this to false.
Any Callback can register to the EntityLifecycle event and get the following payload:
{
entity: string,
id: string,
event: 'CREATED' | 'UPDATED' | 'DELETED'
}If DomainConfiguration.subscription is not disabled API clients can also subscribe to this via GraphQL subscription.
onInit
Callback that is called when this entity is initialized. This is a good place to add hooks or augment any configuration.
Example
In this example we want to allow any client not to query more than 100 car items in any query at once. Therefore we add a hook in the onInit method. If you need the runtime, you can get it via entity.runtime.
export const domainConfiguration: DomainConfiguration = {
entity: {
Car: {
onInit: entity => {
entity.hooks.afterTypesQuery.push( resolved => {
if( ! _.isArray( resolved ) ) throw new Error('wrong resolved value');
if( resolved.length > 100 ) resolved.splice(0, 99);
});
}
}
}
}typeName
The typeName is the name of the GraphQL type in the schema. Per default it is capitalized name of this entity in the domain configuration. Only set this if know very well, what you want to achieve.
entity:
car:
Driver:
typeName: Chauffeurconst domainConfiguration = {
car: {},
Driver: { typeName: 'Chauffeur' }
}Schema (excerpt)
type Car {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
}
type Chauffeur {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
}
type Mutation {
createCar(car: CarCreateInput): SaveCarMutationResult
updateCar(car: CarUpdateInput): SaveCarMutationResult
deleteCar(id: ID): [String]
createChauffeur(chauffeur: ChauffeurCreateInput): SaveChauffeurMutationResult
updateChauffeur(chauffeur: ChauffeurUpdateInput): SaveChauffeurMutationResult
deleteChauffeur(id: ID): [String]
}
type Query {
car(id: ID): Car
cars(filter: CarFilter, sort: CarSort): [Car]
chauffeur(id: ID): Chauffeur
chauffeurs(filter: ChauffeurFilter, sort: ChauffeurSort): [Chauffeur]
}
type SaveCarMutationResult {
validationViolations: [ValidationViolation]!
car: Car
}
type SaveChauffeurMutationResult {
validationViolations: [ValidationViolation]!
chauffeur: Chauffeur
}We suggest to use the capitalized version of the type name as key in the entity object of the configuration.
singular
The singular of an entity is used to infer the name of the type if not stated otherwise. This name is used when referring to an instance of a type. Per default it is determined via inflection of the name of the entity (lower letter). Only set this if you know very well what you want to achieve.
YAML
entity:
Car:
attributes:
brand: string!
Driver:
attributes:
name: string!
assocTo: CarSchema for Car (excerpt)
type Driver {
car: Car
}
input DriverCreateInput {
carId: ID
}
input DriverFilter {
carId: IDFilter
}
input DriverUpdateInput {
carId: ID
}
type Mutation {
createCar(car: CarCreateInput): SaveCarMutationResult
updateCar(car: CarUpdateInput): SaveCarMutationResult
}
type Query {
car(id: ID): Car
}
type SaveCarMutationResult {
car: Car
}plural
The plural of an entity is used to infer the name of a list of types if not stated otherwise. Per default it is determined via inflection of the name of the entity (lower letter). Only set this if you know very well what you want to achieve.
YAML
entity:
Car:
attributes:
brand: string!
assocFrom: Driver
Driver:
attributes:
name: string!
assocTo: CarSchema for Driver (excerpt)
type Car {
drivers: [Driver]
}
type Query {
drivers(filter: DriverFilter, sort: DriverSort): [Driver]
}collection
The collection property defines the collection or database table a data-store should use to read and write entities.
Per default this is the plural of the entity.
path
The path property is the identifier for this entity in the UI, also it is the path in the url.
Per default this is the underscore version of the plural of the entity.
YAML
entity:
TransportFleet:
...Result
// path: transport_fleetsui
Usually you configure the behavior of the ActiveQL Admin UI in the adminConfig of your Angular application. You can however choose to add any static configuration in your entity configuration in ActiveQL as well.
YAML
entity:
Car:
attributes:
color: String
brand: String
age: Int
damage: Int
ui:
listTitle: Fleet
itemTitle: Vehicle
resources:
label:
damage: Reported Damage
index:
fields:
- brand
- color
- ageIn this example the UI would use Fleet instead of Cars to refer to any list of cars and Vehicle instead of Car for any car item.
On the index view the table with the car items would include the given fields in that order, instead of all attributes in unreliable order.
Any ui configuration here will be merged with any configuration in your ActiveQL Admin UI client configuration, with the client's configuration preceding this configuration.
Any entry under resources will be merged to the resources object on AdminUI for this entity.
typeQueryName
The name this entity can be accessed in the API. Default is the typeName with lower first character.