Customization
Directives
For better flexibility the IdentityHub provides a set of schema directives that are ready for use in your extensions to achieve certain goals. Below are the available directives listed and described. They are chainable and will always work on the resolved value of the directive before.
@alias
Returns the value of another field. For example, as shown in the example below, you are now able to create a top level property on the User type that resolves to the value of any other property of that type, even nested ones.
type Manager {
firstName: String!
lastName: String!
}
extend type User {
responsible: String! @alias(path: "manager.lastName")
manager: Manager!
}
@buffer
Takes the parameter format
, which can be one of the values of the enum BufferEncoding (base64
, ascii
, utf8
, utf16le
, ucs2
, latin1
, binary
, hex
). You can use this directive for example to create a string representation of the ldap entryUUID ()here in combination with the
@ldapField directive.
extend type Role {
entryUUID: String! @ldapField(attribute: "entryUUID", binary: true) @buffer(format: "hex")
}
You can create a string representation of the buffer value of any attribute. Of course it should be used for attributes that actually store binary data in LDAP. For example the user attribute publicKey. When retrieving binary attributes, it makes sense to set the binary flag of the @ldapField directive to true. By this, the IdentityHub will fetch the attributes value as buffer already and the '@buffer' directive does not have to transform it from a string to a buffer again. So with a schema definition like this:
extend type User {
publicKey: String @ldapField(attribute: "publicKey", binary: true) @buffer
}
You could query like this:
query SearchUsers {
users {
nodes {
# will return the publicKey value in the default format `base64` -> i.e.
# AQAAAAQAAABMALAAAAAmAUwAZAB2AAAAAABPAFUAPQBJAGQAZQBuAHQAaQB0AHkALgBPAFUAPQBLAGUAbgBvAHgAYQAuAE8AVQA9AHUAcwBlAHIAcwAuAE8APQBkAGEAdABhAAAAQwBOAD0AdABoAHAAbABhAHQAaABlAC4ATwBVAxwfgenkAGUAbgB0AGkAdAB5AC4ATwBVAD0ASwBlAG4AbwB4AGEALgBPAFUAPQB1AHMAZQByAHMALgBPAD0AZABhAHQAYQAAAAEAAAADAAEAbABCVgQAMS4wAEJDAQADQkEBADBCTAIApAFOTjUA9eHeWJ1OMFu
publicKey
# will return the publicKey value encoded as utf8 -> i.e.
# \u0001\u0000\u0000\u0000\u0004\u0000\u0000\u0000L\u0000�\u0000\u0000\u0000&\u0001L\u0000d\u0000v\u0000\u0000\u0000\u0000\u0000O\u0000U\u0000=\u0000I\u0000d\u0000e\u0000n\u0000t\u0000i\u0000t\u0000y\u0000.\u0000O\u0000U\u0000=\u0000I\u0000d\u0000e\u0000n\u0000t\u0000i\u0000t\u0000y\u0000.\u0000O\u0000U\u0000=\u0000K\u0000e\u0000n\u0000o\u0000x\u0000a\u0000.\u0000O\u0000U\u0000=\u0000u\u0000s\u0000e\u0000r\u0000s\u0000.\u0000O\u0000=\u0000d\u0000a\u0000t\u0000a\u0000\
utf8: publicKey(format: utf8)
# will return the publicKey value encoded as hex -> i.e.
# 01000000040000004c00b000000026014c0064007600000000004f0055003d004900640065006e0074006900740079002e004f0055003d004b0065006e006f00780061002e004f0050079002e004f0055003d004b0065006e006f00780061002e004f0055003d00750073006500720073002e004f003d006400610074006100000001000000030001006c0042560400312e300042430100034241010030424c0200a4014e4e3500f5e1de589d4e305bbf7bac5a9e606a8ef58cd634b88b854c1e792e0b8c71a6b0bb76ec4cdea66380c454e03000100014d410800735ba6107f0b94776400505552534146
hex: publicKey(format: hex)
}
}
}
This would be combinable for example with the @url directive, to retrieve the image as data url, ready to use in html.
@ensureArray
This directive takes care, that you will always receive an array when requesting a field. For example you can combine it with the @ldapField directive like so:
extend type User {
firstNames: [String!]! @ldapField(attribute: "givenName") @ensureArray
}
Since we defined the field as array of strings, but we don't know if the ldap attribute givenName
has a value or maybe just a single value, we now made sure, that our field will always return an array, even for a single or none value.
@ensureBoolean
This directive takes care, that you will always receive a correct boolean value (true
or false
) when requesting a field. This may be useful for certain types that are created via REST calls, i.e. to the userapp, and you cannot be sure if corresponding "Boolean" values will be correctly provided. This directives transforms the values true
, 1
, 'true'
and 'TRUE'
to the actual boolean value true
and all other values (including null
and undefined
) to the actual boolean value false
.
extend type UserappTask {
isNewForm: Boolean! @ensureBoolean
}
@fieldset
With the fieldset directive you can gather a set of fields of a type to a single custom field. For example you already defined a set of custom attributes on the User
type like so:
extend type User {
booleanScalar: Boolean @ldapField(attribute: "passwordAllowChange")
stringScalar: String @ldapField(attribute: "givenName")
intScalar: Int @ldapField(attribute: "roomNumber")
dateScalar: String @ldapField(attribute: "loginTime") @ldapTimestamp @formatDateTime(format: "yyyy-MM-dd'T'hh:mm:ss xxx")
resolvedUser: User @ldapField(attribute: "directReports") @resolveDN
resolvedUsers: [User!]! @ldapField(attribute: "directReports") @resolveDN
resolvedGroup: Group @ldapField(attribute: "groupMembership") @resolveDN
resolvedGroups: [Group!]! @ldapField(attribute: "groupMembership") @resolveDN
}
In your UI you may want to just query all custom attributes of a user. That's where the Fieldset
comes in handy. Additionally to your already existing custom attributes, you could define something like:
extend type User {
fieldset1: Fieldset @fieldset(fields: ["stringScalar", "booleanScalar", "intScalar", "dateScalar"])
fieldset2: Fieldset @fieldset(fields: ["resolvedUser", "resolvedUsers", "resolvedGroup"])
combinedFieldset: Fieldset @fieldset(fields: ["testpanel1", "testpanel2"])
}
The Directive takes a parameter fields
, which is a list of field names of the type it is used in. Those fields must really exist and be either a Scalar
type or implement the Labeled
interface (see the graphql playground schema explorer for further insight on that interface and the Fieldset
type). As you can see you can even collect other Fieldset
s within a Fieldset
.
A possible query may look like the following:
query GetUserFieldsets {
userByDN(dn: "cn=example,o=data") {
fieldset1 {
label
description
fields {
name
description
label
type
value
}
}
}
}
As a response ypu would get:
{
data: {
userByDN: {
fieldset1: {
label: "A configured label translation for fieldset1",
description: "A configured description translation for fieldset1"
fields: [
{
name: "stringScalar",
label: "A configured label translation for stringScalar",
description: "A configured description translation for stringScalar"
type: "String",
value: "Max"
},
{
"name": "booleanScalar",
label: "A configured label translation for booleanScalar",
description: null,
type: "Boolean",
value: true
},
{
"name": "intScalar",
label: "A configured label translation for intScalar",
description: null,
type: "Int",
value: 103
},
{
"name": "dateScalar",
label: "A configured label translation for dateScalar",
description: "A configured description translation for dateScalar"
type: "String",
value: "2022-04-26T12:20:11 +00:00"
}
]
}
}
}
}
For Fieldsets the fieldset namespace of our i18n library comes to use. With the translation files you can provide the labels/descriptions of a Fieldset
and the corresponding Field
s. These labels are determined using the name of the type that contains the Fieldset
and the name of the Fieldset
itself. If no language(s) are passed for the Fieldset
in the Query, we use the languages from the Accept-Language header of the request. The fallbackLanguage described above is always added to the languages, if not already present.
Considering the example above, with the addition of passing the preferred languages:
query GetUserFieldsets {
userByDN(dn: "cn=example,o=data") {
fieldset1(locale: ["de", "fr", "es"]) {
label
fields {
name
label
type
value
}
}
}
}
The possible translation keys for the label
of the fieldset would be
<type>.<fieldset>.label
(User.fieldset1.label
)<fieldset>.label
(fieldset1.label
)<type>.<fieldset>
(User.fieldset1
)<fieldset>
(fieldset1
)- If no translation is found, the fieldset name is used.
and for the description
:
<type>.<fieldset>.description
—User.fieldset1.description
<fieldset>.description
—fieldset1.description
- If no translation is found,
null
is used
within the translation namespace fieldset
, after that in the common
namespace.
For the field translations, we use the same logic as above, with the following possible keys for the label
of a field:
<type>.<fieldset>.<field>.label
—User.fieldset1.resolvedUser.label
<type>.<field>.label
—User.resolvedUser.label
<fieldset>.<field>.label
—fieldset1.resolvedUser.label
<field>.label
—resolvedUser.label
<type>.<fieldset>.<field>
—User.fieldset1.resolvedUser
<type>.<field>
—User.resolvedUser
<fieldset>.<field>
—fieldset1.resolvedUser
<field>
—resolvedUser
- If no translation is found, the field name is used.
and for the description
:
<type>.<fieldset>.<field>.description
—User.fieldset1.resolvedUser.description
<type>.<field>.description
—User.resolvedUser.description
<fieldset>.<field>.description
—fieldset1.resolvedUser.description
<field>.description
—resolvedUser.description
- If no translation is found,
null
is used
@formatDateTime
It takes the parameters format
and timeZone
. It let's you transform a date into the desired format in the desired timeZone. For example you have a type Task, that resolves a timestamp for the created
field:
type Task {
created: @formatDateTime(format: "yyyy-MM-ddThh:mm" timeZone: "Europe/Berlin")
}
Defaults are format: yyyy-MM-dd'T'hh:mm:ssZ
and timezone: UTC
.
You can still override format and timezone in queries, so for the above schema example you could query i.e.:
query SearchTaskNodes {
nodes {
created(format: "yyyy-MM-dd" timezone: "America/New_York")
}
}
For further insight on the available formats and timeZones please see https://www.npmjs.com/package/date-fns-tz
@id
Simplifies the usage of our internal ID mechanism (a base64 encoded string containing of <type>:<id>
). Takes a parameter type
, which, if not passed, will use the graphql type of the object containing the id property. For example with a type User you could write:
type User {
id: ID! @ldapField(attribute: "entryUUID") @id
}
This will first retrieve the entryUUID attribute from LDAP, and will then generate a base64 encoded string from User:<entryUUID>
. If you pass the type parameter, for example because a type Task contains its own id, as well as the id of the process it belongs to, you can ensure that the correct type is used:
type Task {
id: ID! @id
processId: ID! @id(type: "Process")
}
@ldapField
This directive can be used to map a field of a type to a certain ldap attribute and therefore resolve its value. It takes the parameters attribute
, which is optional and if left out the name of the field itself will be used as attribute
. Furthermore you can add a list of fallbackAttributes
. If the ldap entry has no value for the original attribute
, the fallbackAttributes
will be used in the order they are declared. Within the ldap query all attributes (attribute
and all fallbackAttributes
will be fetched). The last parameter binary
is a boolean that decides, wether the value will be fetched as binary value (i.e. for usage with the
@buffer directive), or as a string representation. With the type of the field, you can define wether you want a single value or an array. Below you foind some examples on how to use this directive:
extend type User {
# creates a resolver for the attribute 'objectClass' (alias is used as attribute) that returns an array of strings
objectClass: [String!]! @ldapField
# creates a resolver for the attribute 'givenName' that returns a string aliased under the attribute 'firstName'
firstName: String @ldapField(attribute: "givenName")
# creates a resolver for the attribute 'jpegPhoto' that returns a data uri with the content type 'image/jpeg'
photo: URL @ldapField(attribute: "jpegPhoto") @url(contentType: "image/jpeg")
# creates a resolver for the attribute 'date' that returns the ldap timestamp in the given format
date: String @ldapField @ldapTimestamp @formatDateTime(format: "yyyy-MM-dd'T'hh:mm:ss xxx")
}
@ldapFind
This directive can be used to create a query resolver for the returned type and according to the passed parameters. The base
parameter defines the ldap base DN for the search operation. It accepts either a real DN or a string in the form of attribute:<attributeName>
which will use the value of the given ldap attribute of the root object as DN. With scope
you can control the ldap search scope and filter
let's you define additional custom ldap filters that will be combined with logical AND
with other parameters like where
, by
or q
that you may allow for that field. Additionally you can define a set of qAttributes
(a list of attribute names) that will be matched with the value for the q
parameter (equality). And with disablePaging
you are able to turn off the ldap paged search and instead search with a size limit of 2. The ldap find will return any found entry as long it will find exactly one result. In case of no found entry or multiple entries matching the filter criteria, the resolver of this directive will retun null
. With this example:
input ApplicationWhere {
_and_: ApplicationWhere
_or_: ApplicationWhere
_not_: ApplicationWhere
category: [StringMatchFilterInput!]
cn: StringFilterInput
applicationOwner: [StringMatchFilterInput!]
applicationName: [StringMatchFilterInput!]
}
extend type Query {
application(where: ApplicationWhere): Application @ldapFind(filter: "(&(objectClass=application)(appType=application))", scope: sub, base: "ou=applications,o=data")
}
you created a new query, that allows you to find a certain ldap entry of the examplary type Application
. The filter
must always match and is combined with the custom ApplicationWhere
which can be passed at query time and creates a corresponding additional filter.
@ldapFindByAttribute
Can be used to create a base query resolver for the returned type and according to the passed parameters. The parameter attribute
defines the attribute to match with the resolved value of the field, i.e.
extend type Role {
application: Application
@ldapField(attribute: "nrfRoleCategoryKey")
@ldapFindByAttribute(base: "ou=applications,o=data", scope=sub, attribute: "appCategory", filter="(objectClass=application)")
}
This will first resolve the attribute nrfRoleCategoryKey
of the Role
via ldap and then find any Application
that has this value as appCategory
(exact match).
@ldapFindByDn
This directive can be used to create a resolver for a field that can resolve a complex type via ldap by its DN. There are multiple ways to use this directive with its several parameters. For example you may use the dn
parameter:
extend type Query {
applicationByDN(dn: DN!): Application @ldapFindByDN
}
This creates a new query applicationByDN
(Application
is an examplary ldap type) that you must pass a DN, which is then used to perform a ldap base search with that value and returns the entry if it is found.
It would be also possible to create a static query by passing a DN in the schema extension:
extend type User {
manager: User @ldapFindByDn(dn: "cn=manager, ou=users,o=data")
}
In that case the manager
field of a User
will always resolve to the same manager for all users.
Additionally you can use the path
parameter:
extend type User {
managerDn: DN
manager: User @ldapFindByDn(path: "managerDn")
}
This will take the managerDn
field of the resolved User
and will use that to resolve the manager
field of the same User
.
@ldapFindByEntryUuid
Can be used to create a query resolver to find an entry by its entryUUID
attribute under the given base
, the given scope
and optionally an additional filter
. For example:
extend type Query {
applicationByEntryUUID(entryUUID: ID!): Application @ldapFindByEntryUuid(base: "ou=applications,o=data")
}
This creates a query applicationByEntryUUID
that you must pass an ldap entryUUID
and that will try to find an entry of type Application
below the given base
with the given entryUUID
.
@ldapFindById
Works the same as the
@ldapFindByEntryUuid directive with the only difference that not the ldap internal entryUUID
is used, but the IdentityHub internal id
that is a base64 representation of the type
and the id
of that type. (see the
@id directive#id)
@ldapSearch
This directive let's you create a query resolver that performs a ldap search operation and resolves a Connection
type based on the passed parameters base
, scope
, filter
and qAttributes
. The base
parameter defines the ldap base DN for the search operation. It accepts either a real DN or a string in the form of attribute:<attributeName>
which will use the value of the given ldap attribute of the root object as DN. The scope
defines which ldap search scope is used and filter
let's you define additional ldap filters that will be always joined with any other passed filters (via by
, where
or q
query) with an AND
operation. For example taken a custom type Application
, you can define a query to search for those types:
input ApplicationWhere {
_and_: ApplicationWhere
_or_: ApplicationWhere
_not_: ApplicationWhere
category: [StringMatchFilterInput!]
cn: StringFilterInput
applicationOwner: [StringMatchFilterInput!]
applicationName: [StringMatchFilterInput!]
}
extend type Query {
applications(q: String, where: ApplicationWhere) @ldapSearch(base: "ou=applications,o=data", scope: sub, filter: "(objectClass=application)", qAttributes: ["applicationName", "applicationDescription"])
}
This query performs a ldap search operation with scope
sub
below the given base
and will only find entries that match the objectClass application
. Additionally we defined a q
parameter for that query. By using it you can narrow down your results by the automatically generated additional ldap filter that will take the value of q
and will match it against the given qAttributes
. For a more complex use case we also defined the ApplicationWhere
which can provide a more flexible way of creating filters (see the schema docs in the playground for more details).
@ldapTimestamp
Transforms the ldap timestamp to ISO conform timestamps. For formatting you can combine this directive with the @formatDateTime directive:
type User {
lastLoginDate: String @ldapField(attribute: "loginTime") @ldapTimestamp @formatDateTime(format: "yyyy-MM-dd" timeZone: "Europe/Berlin")
}
@localizeLdapAttribute
Use this directive for ldap multi language attributes to extract the language you need. Takes the parameters localize
(can be set to false in a query if you need the whole string for some reason) and locale
(define the preferred language to extract). The locale
will, if not passed as parameter use the requests accept-language
header to determine your browser language and will fall back to the configured i18n.fallbackLanguage
if nothing else is found. Given the example value of the attribute nrfLocalizedNames
of a role as de~deutscher Name|en~english name
and a schema definition like below
type Role {
name: @ldapField(attribute: "nrfLocalizedNames") @localizeLdapAttribute
}
you could query like this (assume that the language from the accept-language header will result in de
):
query SearchRoles {
roles {
nodes {
name # returns 'deutscher name'
englishName: name(locale: "en") # returns 'english name'
italianName: name(locale: "it") # returns 'english name' (fallback)
unlocalizedName: name(localize: false) # returns 'de~deutscher Name|en~english name'
}
}
}
@resolveDn
Can be used to create a query resolver to find an entry by its DN using a ldap base search. The parameter path
defines which property of the parent object should be resolved. This direcxtive also can handle multi value fields. You decide wether you want to receive a single resolved type or an array in your schema extension. For example you can extend a User:
extend type User {
resolvedGroup: Group @ldapField(attribute: "groupMembership") @resolveDN
resolvedGroups: [Group!]! @ldapField(attribute: "groupMembership") @resolveDN
}
Both queries will use the resolved value of the @ldapField
directive and will start to resolve the DNs that are saved in the multi value attribute groupMembership
. The first field resolvedGroup
will only resolve the first DN found in groupMembership
, while the second one will resolve all of them in an array.
Alternatively you can use the additional path
parameter. If the @resolveDN
directive comes first and there is nor former resolved value, we will use the value of the attribute defined by path
. For example:
extend type User {
manager: DN
resolvedManager: User @resolveDN(path: "manager")
}
will take the DN from the User
s manager
field and resolve that for the resolvedManager
field.
@resolveDnConnection
Can be used to create a query resolver to find a entries by their DN using a ldap base search and returns them as connection. The parameter path
defines which property of the parent object should be resolved (if there is not alredy another resolver before @resolveDnConnection
in which case the resolved value would be used). The resolvedType
parameter can be set if you are sure of what type the results will be. In case the returned type will be i.e. a union, leave this parameter out. It will get inferred by objectClass queries for every node. Because this will return a Connection
, you should take care that the query results are pageable:
extend type User {
groupMembership: [DN!]! @ldapField
resolvedGroups1(paging: LDAPPaging): GroupConnection! @resolveDNConnection(path: "groupMembership", resolvedType: "Group")
resolvedGroups1(paging: LDAPPaging): GroupConnection! @ldapField(attribute: "groupMembership") @resolveDNConnection(resolvedType: "Group")
}
The fields resolvedGroups1
and resolvedGroups2
in the example above, will do exactly the same. The first one uses the path
parameter to use the value from the groupMembership
field, the second one takes care to resolve that value on its own by using the @ldapField
directive. Since we already know that we will resolve a connection of Group
s, we can pass that type via the resolvedType
parameter. This will improve the performance of the query since we do not need to infer the type via seperate ldap queries:
@url
Takes the parameters contentType
and encoding
. Can be used i.e. to create a data url for a user avatar image that can be used directly in the src attribute of an image tag in the browser:
type User {
photo: URL @ldapField(attribute: "jpegPhoto", binary: true) @url(contentType: "image/jpeg")
homepage: URL @ldapField(attribute: "homepage", binary: true) @url(contentType: "text/html")
}
@ldapInput
This directive can be used to provide the ldap attribute name for the input fields. It can be useful to define inputs for ldap mutations where the field names do not match the ldap attributes.
For example you may define an input to modify a User:
input UserChange {
title: String @ldapInput(attribute: "displayName")
room: Int @ldapInput(attribute: "roomNumber" )
mail: String @ldapInput # default attribute mail retrieved from schema
name: String @ldapInput # default attribute cn retrieved from schema
}
By using the directive you can define custom fieldNames and provide the real ldap attribute names that are then used for the modification. If the directive is not used in this case, the attribute is extracted from the schema defaults.
@ldapAdd
Can be used to create a mutation resolver for a ldap modify operation.
It relies on a parameter called input
that defines the attributes of the ldap object to create.
Additionally there must be a parameter called rdn
that defines the relative distinguished name of the object.
The base DN is extracted from the configuration for the return type.
Below is an example of how to use this directive in a schema extension:
input AddUser {
objectClasses: [String!]! @ldapInput(attribute: "objectClass")
firstName: String! @ldapInput(attribute: "givenName")
lastName: String! @ldapInput(attribute: "sn")
mail: [String!] @ldapInput(attribute: "mail")
}
extend type Mutation {
addUser(rdn: DN!, input: AddUser!): User @ldapAdd
}
The also used directive @ldapInput
is used here to provide the ldap attribute name for the input fields.
If left out, the field name is used as the ldap attribute name.
You would use the mutation like this:
mutation addUser {
addUser(
rdn: "cn=new-user,ou=identity"
input: {
objectClasses: ["Top", "Person", "inetOrgPerson", "organizationalPerson"],
firstName: "Max",
lastName: "Mustermann"
}
) {
dn
firstName
lastName
}
}
If the configured base for the User type is i.e. ou=users,o=data
the user would be created at cn=new-user,ou=identity,ou=users,o=data
.
@ldapModify
Can be used to create a mutation resolver for a ldap modify operation.
It relies on a parameter called input
that defines which attributes can be changed.
The input
parameter must be an object with the fields operation
and changes
.
It is also possible to send an array of input
s.
Below is an example of how to use this directive in a schema extension:
input UserChange {
title: String @ldapInput(attribute: "displayName")
room: Int @ldapInput(attribute: "roomNumber" )
mail: String @ldapInput
fullName: String @ldapInput
}
input UserModification {
operation: LDAPChangeOperation!
changes: UserChange!
}
extend type UserMutations {
modify(input: [UserModification!]!): User @ldapModify
}
The also used directive @ldapInput
is used here to provide the ldap attribute name for the input fields.
If left out, the field name is used as the ldap attribute name.
You would use the mutation like this:
mutation modify {
user(by: {id: "AVV234UZVTZV34HIG547658"}) {
modify(input: [
{operation: replace, changes: {title: "Dr.", room: 1234}},
{operation: add, changes: {mail: "[email protected]"}},
{operation: delete, changes: {mail: "[email protected]"}},
]) {
dn
title
room
mail
fullName
}
}
}
As you can see, you can send multiple changes in one request. The operation can be one of replace
, add
or delete
.
@ldapModifyDn
Can be used to move a ldap object.
It relies on a parameter called rdn
that (together with the defined return type) defines the new dn of the entry.
Below is an example of how to use this directive in a schema extension:
extend type UserMutations {
move(rdn: DN!): User @ldapModifyDn
}
You would use the mutation like this:
mutation modifyDn {
user(by: {id: "AVV234UZVTZV34HIG547658"}) {
move(rdn: "cn=Max Mustermann,ou=internal") {
dn
firstName
lastName
}
}
}
If i.e. the configured base for the User
type is ou=users,o=data
the user would be moved to cn=Max Mustermann,ou=internal,ou=users,o=data
.
@ldapDelete
Can be used to delete a ldap object.
Below is an example of how to use this directive in a schema extension:
extend type UserMutations {
delete: DN! @ldapDelete
}
You would use the mutation like this:
mutation delete {
user(by: {id: "AVV234UZVTZV34HIG547658"}) {
delete {
deletedDN
}
}
}
deletedDN
The operation returns the @rest
To be able to use this directive properly, you have to add backend configurations for every query or mutation that uses REST APIs. An examplary config is shown below:
[[rest]]
# the name of the backend
name="example"
# the base url of the REST backend
base="https://api.rest.dev"
# default headers that are send with every request
[rest.headers]
authorization="Basic xyz"
"X-example"=["example", "xxx"]
# default query parameters that are send with every request
[rest.query]
api="v2"
"searchAttributes"=["name", "label"]
# default parts of the payload that are send with every request
[rest.body]
startedBy="IdentityHub"
pageSize=12
# You can add more config values to this section that will be accessible via the $options selector inside the @rest parameters
apiVersion="v2"
The rest
config is an array and can contain as many REST backends as needed. Mandatory for a REST backend config is the name and the base url. Additionally you can define headers, query params and body that are used as default values for the headers, query params and payload for any request performed with the configured query or mutation. The values for headers and query can contain strings or arrays of strings, the values for the body can be anything.
Now that we have configured our example
backend, we can create our schema-extension:
"""
Given that the original REST Response looks like this:
\`\`\`json
{
"success": true | false,
"error": "An error occurred"
}
\`\`\`
Here we also map the \`error\` property to our field \`message\`
"""
type RestExampleResponse {
success: Boolean!
message: String @alias(path: "error")
}
extend type Mutation {
restExample(search: String, objectid: String): RestExampleResponse
@rest(
backend: example,
endpoint: "/objects/{{$args.objectId}}",
request: {
method: post,
headers: { "X-api-version": "{{$options.apiVersion}}" },
query: { search: "{{$args.search}}", useProxyAuth: "{{$config.graphql.useProxyAuth}}" },
body: { dn: "{{$viewer.dn}}" },
}
)
}
The directive provides ways to define values for the headers, query and body part, by using one of the following keywords:
$parent
: This accesses the root object of the field resolver. The key is the path to the value in the root object. The path is separated by dots. For example $root.dn
accesses the value of the field dn
of the root object.
$args
: This accesses the arguments of the field resolver. The key is the path to the value in the arguments object. The path is separated by dots. For example $args.additionalHeader
accesses the value of the argument additionalHeader
.
$config
: This can access and use any value from the config. The key is the path to the value in the config object. The path is separated by dots. For example $config.graphql.useProxyAuth
accesses the value of the config key graphql.useProxyAuth
.
$options
: This can access and use any value from the config part of the specific service. The key is the path to the value in the array entry of the config object. The path is separated by dots. For example $options.apiVersion
accesses the value of the config key apiVersion
of the used backend.
$viewer
: This accesses the oAuth UserInfo Object (type AuthInfo
). The key is the path to the value in the UserInfo object. The path is separated by dots. For example $viewer.dn
accesses the value of the UserInfo key dn
.
When using these keywords inside double curly brackets (i.e. {{$args.id}}
), a string interpolation is performed. The value inside the brackets is replaced with the actual value of the keyword.
For more flexibility the keywords can be used without the brackets. In this case the actual value can also be an array or object or anything that it refers to.
For example a mutation defines a parameter ids
which contains an array of ids. This array should be used as payload for a DELETE request:
type DeleteResponse {
success: Boolean!
message: String @alias(path: "error")
}
extend type Mutation {
delete(ids: [String!]!): DeleteResponse
@rest(
backend: example,
endpoint: "/objects",
request: {
method: delete,
body: "$args.ids",
}
)
}
The restExample
mutation can be used like this:
mutation example {
restExample(apiKey: "abc", search: "xyz", objectId: "1") {
success
message
}
}
Via choosing the backend example
along with the endpoint /example/$args.objectId
, the url we send the request to will be:
https://my-rest-api.org/v1/example/1.
Since we configured default query params and also additional query params in the directive args, as well as given, that our configured value for [graphql.useProxyAuth] is false, the query parameters we will send with the request will be:
?example=true&additionalQueryParam=xyz&useProxyAuth=false
Since we configured default headers and also additional headers in the directive args, the headers we will send with the request will be:
authorization="Basic 2343UFCZkjb4657"
"X-example"="example"
"X-api-version"="v2"
Since we configured a default body and also an additional body in the directive args, the final payload we will send with the request will be:
{
"example": true,
"dn": "cn=John Doe,ou=People,dc=example,dc=com"
}
The DN for the dn
is extracted from the oAuth userInfo object.
The directive will throw an error if the request fails or the status code of the response is not 2xx. The error will contain the response code and the error message from the REST backend.