1. Customization
  2. Directives

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.

graphql
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.

graphql
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:

graphql
extend type User {
  publicKey: String @ldapField(attribute: "publicKey", binary: true) @buffer
}

You could query like this:

graphql
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:

graphql
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.

graphql
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:

graphql
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:

graphql
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 Fieldsets within a Fieldset.

A possible query may look like the following:

graphql
query GetUserFieldsets {
  userByDN(dn: "cn=example,o=data") {
    fieldset1 {
      label
      description
      fields {
        name
        description
        label
        type
        value
      }
    }
  }
}

As a response ypu would get:

graphql
{
  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 Fields. 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:

graphql
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>.descriptionUser.fieldset1.description
  • <fieldset>.descriptionfieldset1.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>.labelUser.fieldset1.resolvedUser.label
  • <type>.<field>.labelUser.resolvedUser.label
  • <fieldset>.<field>.labelfieldset1.resolvedUser.label
  • <field>.labelresolvedUser.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>.descriptionUser.fieldset1.resolvedUser.description
  • <type>.<field>.descriptionUser.resolvedUser.description
  • <fieldset>.<field>.descriptionfieldset1.resolvedUser.description
  • <field>.descriptionresolvedUser.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:

graphql
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.:

graphql
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:

graphql
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:

graphql
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:

graphql
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:

graphql
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.

graphql
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:

graphql
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:

graphql
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:

graphql
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:

graphql
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:

graphql
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:

graphql
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

graphql
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):

graphql
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:

graphql
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:

graphql
extend type User {
  manager: DN
  resolvedManager: User @resolveDN(path: "manager")
}

will take the DN from the Users 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:

graphql
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 Groups, 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 inputs.

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
    }
  }
}

The operation returns the deletedDN

@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.