Licensed to be used in conjunction with basebox, only.
@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
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" } }
)
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 ID
s. 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:
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"
}
}
]
)
}
_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:
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:
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:
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.