Skip to content

@bb_resolver

@bb_resolver defines how a GraphQL operation should be resolved - or "carried out" in other terms.

Syntax

OperationDefinition
    @bb_resolver(
      _type: TypeSpecifier
          TypeSpecifierArguments
    )

Where:

OperationDefinition

Definition of the GraphQL operation; this is standard GraphQL syntax, see here

TypeSpecifier

Specifies the type of resolver function to perform for this operation.
Type: Enum
Value: One of Select | Insert | InsertNested | Update | UpdateNested | Delete | Orchestrator | AuthManagement | HttpService

Depending on the type specified with TypeSpecifier, a set of arguments are required and/or available (TypeSpecifierArguments).

DB Operation Arguments

Definition of arguments for TypeSpecifier = Select | Insert | InsertNested | Update | UpdateNested | Delete.

_object

ObjectType; a previously defined type that this operation should primarily operate on. For instance, if this operation should create an object, specify the object's type here.

_filter

ObjectFilterSpecifier; defines a filter that limits the objects this operation is applied to. Only applicable if TypeSpecifier is Select | Update | UpdateNested | Delete.

_fields

FieldsSpecifier; specifies which fields are available or the operation and where their content comes from.

_grant_access

GrantAccessSpecifier; provides possiblity to grant access to specific data records to a specific user.

_object

The type of object this operation works on. Refers to a type defined before.

Example:

type Patient {
  id: ID,
  name: String!
}

type query {
  # gets a single patient record given the patient ID
  getPatient(patientId: ID!): Patient
  @bb_resolver(
      _type: SELECT
      _object: Patient
      _filter: { id: { _eq: "$patientId" } }
  )
}

_filter

Specifies filter settings for operations that require a selection set; uses GraphQL object syntax.

Syntax:

{
  fieldName: {
    comparison-specifier: value
  }
}
fieldName

Name of the field to filter by or search. This field must be defined in the object type specified in _object.

Comparison Specifier

Specifies how the comparison should be performed. See list below.

_eq

field must match value

_neq

field must not match value.

_gt

field must be greater than value; strings must sort behind value.

_gte

field must value; strings must sort same or behind value.

_lt

field must value; strings must sort before value.

_lte

field must value; strings must sort same or before value.

_in

field must be contained in value.

_nin

field must not be contained in value.

_like

field must be LIKE value; allows a wildcard search using % as the wildcard.

_nlike

field must not be LIKE value; allows a wildcard search using % as the wildcard.

Example:

type query {
  activePatients(nameLike: ID!): [Patient]
  @bb_resolver(
      _type: SELECT
      _object: Patient
      _filter: { 
        name: { 
          _like: "$nameLike" 
        }
        active: {
          _eq: "true"
        } 
      }
  )
}

_fields

Examples

Basic SELECT resolver example
type Patient {
  id: ID!
  name: String
}

# gets a single patient record given the patient ID
getPatient(patientId: ID!): Patient
@bb_resolver(
    _type: SELECT
    _object: Patient
    _filter: { id: { _eq: "$patientId" } }
)
In this simple example, we define a getPatient operation that gets a single patient given the patient ID. The resolver would query the patient table in the database corresponding to the Patient object in the GraphQL schema and fetch a patient record for the ID provided.

Basic INSERT resolver example using an input object
type Patient {
  id: ID!
  firstName: String
  lastName: String
  age: Int
}

input PatientInput {
  firstName: String
  lastName: String
  age: Int
}

type Mutation {
  # inserts a patient record given a patient input object
  insertPatient(patientData: PatientInput!): Patient
    @bb_resolver(
      _type: INSERT
      _object: Patient
      _fields: { 
        firstName: "$patientData.$firstName"
        lastName: "$patientData.$lastName"
        age: "$patientData.$age"
      }
    )
}

In this example we are performing an insert into the Patient table. We are also using an input object instead of scalar arguments to provide the insert data; this is done purely for illustration purposes on using input objects. Note the format of the arguments when using input objects (i.e. $input_object.$input_object_field_argument).

_grant_access

Examples

In the below sample we have a GraphQL mutation insertPatientData which inserts a PatientData object into the database and grants access to a specific DocUser user.

A DocUser can load the PatientData he has access to with the GraphQL query getPatientData.

scalar Void @bb_scalar(type: "Void")

type PatientData @bb_owned_with_access {
    id: ID!
    data: String!
    anyOtherThing: String
}

input InsertPatientData {
    data: String!
    anyOtherThing: String
}

type PatientUser @bb_user {
    id: ID!
    firstName: String!
    lastName: String!
    gender: String!
    age: String!
}

type DocUser @bb_user {
    id: ID!
    firstName: String!
    lastName: String!
    salutation: String!
    medicalSpeciality: String!
}

type Query {
    getPatientData: [PatientData!]
    @bb_resolver(
        _type: SELECT
        _object: PatientData
    )
}

type Mutation {
    insertPatientData(dataToInsert: InsertPatientData, assignedDocId: ID!): PatientData
    @bb_resolver(
      _type: INSERT,
      _object: PatientData,
      _grant_access: {
          access_ids: "$assignedDocId"
          user_type: DocUser
      },
      _fields: {
          data: "$dataToInsert.$data"
          anyOtherThing: "$data.$anyOtherThing"
      }
    )
}

In access_ids of the _grant_access object we expect ID item(s) of the User(s) which should be able to access the specific record of a specific _type. In our sample, the expected ID is DocUser.id.

Internally we will map the DocUser.id to the IdP user id and insert it as an array item to the .accessIds column of the PatientData.

Note

Because PatientData is bb_owned_with_access the compiler knows that it is an owned data record, owned by a creator, with the possibility to grant access to specific other users.

Because the IdP of a DocUser is in the .accessIds of a PatientData record, the corresponding DocUser can load the PatientData he granted access to.

In access_ids, we allow single ID or an array of IDs. We do not allow any other GraphQL types. Referencing child ID attributes of a GraphQL input is also not allowed yet and results in an error during schema compilation (bbc).

Within user_type we expect a type from our schema with @bb_user annotation representing a mapping between application and IdP user. If any other GraphQL type without @bb_user annotation is passed, the compiler will throw an error.

The _grant_access object can only be used on INSERT, INSERT_NESTED, UPDATE and UPDATE_NESTED operations.

_grant_access cannot be used together with @bb_roles because @bb_roles grants access to all @bb_user specified in the resolver.

Note

The access to a record can only be changed by the owner of the record.

The access can be updated or revoked for a specific PatientData record within a further mutation operation, fe. looking like this:

updateAccessToPatientData(assignedDocId: ID, patientDataId: ID!): PatientData
@bb_resolver(
  _type: UPDATE,
  _object: PatientData,
  _filter: { id: { _eq: "$patientDataId" } }
  _grant_access: {
      access_ids: "$assignedDocId"
      user_type: DocUser
  }
)

That would give the possibility to grant another specific DocUser access to the PatientData. Or revoke the access when assignedDocId is submitted with null.

To grant access to a specific PatientData record to multiple DocUser users, we would write a mutation looking like this:

updateAccessToPatientData(assignedDocsId: [ID!], patientDataId: ID!): PatientData
@bb_resolver(
  _type: UPDATE,
  _object: PatientData,
  _filter: { id: { _eq: "$patientDataId" } }
  _grant_access: {
      access_ids: "$assignedDocsId"
      user_type: DocUser
  }
)

To change access grants, updateAccessToPatientData can be called with different assignedDocsId including new users who should get access or excluding users who should be removed.

Note

To keep access for the users, the corresponding IDs have to be included in the submitted array.

For INSERT_NESTED, the access defined within _grant_access on the operation will also be available on the child objects.

In the below example, insertPatientData is fills the .accessIds columns of PatientData and BloodTest and grants the corresponding DocUser access.

....

type PatientData @bb_owned_with_access {
    id: ID!
    data: String!
    anyOtherThing: String
    bloodTests: [BloodTest!]
}

type BloodTest @bb_owned_with_access {
    id: ID!
    name: String!
    value: String!
}

input InsertBloodTest {
    name: String!
    value: String!
}

input InsertPatientData {
    data: String!
    anyOtherThing: String
    bloodTest: [InsertBloodTest!]
}

....

type Mutation {
    insertPatientData(dataToInsert: InsertPatientData, assignedDocId: ID!): PatientData
    @bb_resolver(
      _type: INSERT_NESTED,
      _object: PatientData,
      _grant_access: {
          access_ids: "$assignedDocId"
          user_type: DocUser
      },
      _fields: {
          data: "$dataToInsert.$data"
          anyOtherThing: "$data.$anyOtherThing"
          bloodTests: [
          {
            name: "$dataToInsert.$bloodTest.$name"
            value: "$dataToInsert.$bloodTest.$value"
          }
          ]
      }
    )

    ....
}

For UPDATE_NESTED, it is working the same.

To update or revoke the access we would need to change BloodTest and PatientData .accessIds column. To do so, we would write 2 mutations and an ORCHESTRATOR mutation which we make available to the caller:

scalar Void @bb_scalar(type: "Void")

....

type Mutation {
....

    updateAccessToPatientData(assignedDocsId: [ID!], patientDataId: ID!): PatientData
    @bb_resolver(
      _type: UPDATE,
      _object: PatientData,
      _filter: { id: { _eq: "$patientDataId" } }
      _grant_access: {
          access_ids: "$assignedDocsId"
          user_type: DocUser
      }
    )

    updateAccessToBloodTest(assignedDocsId: [ID!]!, bloodTests: [ID!]!): PatientData
    @bb_resolver(
      _type: UPDATE,
      _object: BloodTest,
      _filter: { id: { _in: "$bloodTests" } }
      _grant_access: {
          access_ids: "$assignedDocsId"
          user_type: DocUser
      }
    )

    updateAccessToPatientDataAndBloodTests(assignedDocsId: [ID!]!, bloodTests: [ID!]!, patientDataId: ID!): Void
    @bb_resolver(
        _type: ORCHESTRATOR
        _steps: [
            {
                _mutation: "updateAccessToPatientData"
                _arguments: {
                    assignedDocsId: "$assignedDocsId"
                    patientDataId: "$patientDataId"
                }
            }
            {
                _mutation: "updateAccessToBloodTest"
                _arguments: {
                    assignedDocsId: "$assignedDocsId"
                    bloodTests: "$bloodTests"
                }
            }
        ]
    )

....
}

Orchestrator

The orchestrator type allows you to join multiple other operations into one operation. Here's a basic example to illustrate the point:

Orchestrator example
type Mutation {
  insertSubscription(name: String!, amount: Float!): Subscription
    @bb_resolver(
      _type: DELETE
      _object: Subscription
      _filter: { id: { _eq: "$id" } }
    )

  deleteSubscription(subscription_id: ID!): Void
    @bb_resolver(
      _type: DELETE
      _object: Subscription
      _filter: { id: { _eq: "$subscription_id" } }
    )

  replaceSubscription(old_id: ID!, new_name: String!, new_amount: Float!): Void
    @bb_resolver(
      _type: ORCHESTRATOR
      _steps: [
        {
          _mutation: "deleteSubscription"
          _arguments: {
            subscription_id: "$old_id"
          }
        }
        {
          _mutation: "insertSubscription"
          _arguments: {
            name: "$new_name",
            amount: "$new_amount"
          }
        }
      ]
    )
}
An orchestrator has a _steps specifier that defines the different operations involved. There are two steps in the example above. Each step is furthermore made up of the operation name (indicated by the _mutation specifier) and the _arguments specifier that allow you to map arguments from the orchestrator operation to the arguments of the step operation.

Note

Nested orchestrators, i.e. orchestrators within another orchestrator, is not supported. ORCHESTRATOR operations should always be mutations.

In an orchestrator, we can have query and mutation steps. We can access data from a previous step by using a _selection attribute which contains a query.

In the below sample we create a PetSnapshot from a Pet with orchestratorGetPetAndCreateSnapshot. 1st we query a Pet and retrieve its data in the _selection. Then we insert the snapshot from the query data.

scalar Void @bb_scalar(type: "Void")

type Pet @bb_owned {
  id: ID!
  name: String!
  description: String!
  dog: Boolean!
  owner: String
  address: Address!
}

type PetSnapshot @bb_owned {
    id: ID!
    petData: String!
}

type Address @bb_owned {
  id: ID!
  street: String
  postalCode: String
  city: String
}

type Query {
    getPetById(id: ID!): Pet
    @bb_resolver(
      _type: SELECT,
      _object: Pet,
      _filter: { id: { _eq: "$id" } }
    )
}

type Mutation {
    insertPetSnapshot(data: String!): Snapshot
    @bb_resolver(
      _type: INSERT,
      _object: PetSnapshot,
      _fields: {
        data: "$data"
      }
    )

    orchestratorGetPetAndCreateSnapshot(petId: ID!): Void
    @bb_resolver(
        _atomic: true
        _type: ORCHESTRATOR
        _steps: [
            {
                _query: "getPetById"
                _arguments: {
                    id: "$petId"
                }
                _selection: """
                    query {
                        getPetById {
                            id
                            owner
                            dog
                            address {
                              id
                              postalCode
                              city
                            }
                        }
                    }
                """
            }
            {
                _mutation: "insertPetSnapshot"
                _arguments: {
                    data: "_query.getPetById"
                }
            }
        ]
    )
}

The data inserted is a JSON String like the query response of getPetById would look like:

{ 
  "data": { 
    "getPetById": { 
      ... 
    }
  }
}

If executing a GraphQL query in an orchestrator step, the call has to be prefixed with _query. If executing a mutation, we have to use _mutation.

_mutation steps can also have _selection attributes. In the below sample we use the updatePet mutation in orchestratorUpdatePetAndCreateSnapshot to 1st update some attributes of Pet and then insert a PetSnapshot.

....

type Mutation {
    ....

    updatePet(id: ID!, name: String!, description: String!): Pet
    @bb_resolver(
      _type: UPDATE,
      _object: Pet,
      _filter: { id: { _eq: "$id" } }
      _fields: {
        name: "$name"
        description: "$description"
      }
    )

    orchestratorUpdatePetAndCreateSnapshot(petId: ID!): Void
    @bb_resolver(
        _atomic: true
        _type: ORCHESTRATOR
        _steps: [
            {
                _mutation: "updatePet"
                _arguments: {
                    id: "$petId"
                    name: "Chip"
                    description: "A cute tri color australien sheppard."
                }
                _selection: """
                    mutation {
                        updatePet {
                            id
                            owner
                            dog
                            address {
                              id
                              postalCode
                              city
                            }
                        }
                    }
                """
            }
            {
                _mutation: "insertPetSnapshot"
                _arguments: {
                    data: "_mutation.updatePet"
                }
            }
        ]
    )
}

From data returned within a graphql query of _selection we can also pick single attributes out. Assuming we want to execute step insertPetSnapshot but only pick the Pet.address we would have to change to _mutation.updatePet.address.

It is possible to pick single fields and chain them with +, fe. we could get _mutation.updatePet.owner + _mutation.updatePet.address.postalCode which would result in a JSON String like this:

{ 
  "data": { 
    "getPetById": { 
      "owner": "Donald", 
      "address": { 
        "postalCode": "12345"
      }
    }
  }
}

If we try to query GraphQL fields in _selection which do not exist, we will get an error on compilation level by the bbc. If we try to query a field in _arguments of insertPetSnapshot step which are not queried, there will be a compilation error as well thrown by the bbc (fe. if we try to use _mutation.updatePets or _mutation.updatePet.notExistingAttribute or _query.updatePets).

If there is an @bb_restrict_fields resolver added to an operation:

    updatePet(id: ID!, name: String!, description: String!): Pet
    @bb_restrict_fields(_fields: ["address"])
    @bb_resolver(
      _type: UPDATE,
      _object: Pet,
      _filter: { id: { _eq: "$id" } }
      _fields: {
        name: "$name"
        description: "$description"
      }
    )

But we try to access any fields marked as restricted inside the _selection attribute:

....
mutation {
    updatePet {
        id
        owner
        dog
        address {
          id
          postalCode
          city
        }
    }
}
....

We get an error from the bbc saying that this is not allowed as address is marked as restricted on updatePet mutation operation.

AuthManagement

See AuthManagement.

HttpService

Define a microservice that should resolve the operation.

The microservice gets the unmodified GraphQL request from basebox and must obviously be able to handle it and return a proper JSON response.

See also the Microservices Guide.

Arguments

_url

The URL of the GraphQL endpoint basebox should POST the GraphQL request to.

_passthrough_headers

A list of HTTP header names that should be passed through to the microservice.
Type: List of Strings
Example: [ "X-Forwarded-For", "X-Forwarded-Proto" ]
If a header name is not found in the original request, broker uses the string --header not found in client request-- for this header name instead.

GraphQL Request

basebox POSTs the current GraphQL request as-is to the microservice and returns its response to the client without modifying it.

The current user's access token is added to the Authorization HTTP header field as a base64 encoded string.

Security Warning

The microservice is responsible to verify that the user has all required access rights to perform the operation. This must be done by looking at the fields in the decoded access token.