GraphQL in the SEEK API

GraphQL in the SEEK API

Operations

The SEEK API supports two types of GraphQL operations: queries and mutations.
  • Queries allow you to read objects.
  • Mutations allow you to create, update and delete objects.
This developer site provides guided examples of the queries and mutations relevant to each use case. A comprehensive lower-level listing is available in our schema documentation .
The SEEK API does not make use of GraphQL subscriptions and instead provides webhooks.

Scheme identifiers

Scheme identifiers indicate the context that a given object belongs to and form the basis of the SEEK API’s multi-market support. They are predominantly seen in object identifiers, and are also input into certain operations to denote a specific market that information is being requested from or submitted to.
There are three scheme IDs currently in use:
  • seekAnz is used for objects specific to SEEK’s Australia & New Zealand market.
  • global is used for objects we envision being used across the SEEK Group.
  • seekAnzPublicTest and globalPublicTest are used for mock data in our GraphQL Playground.
    Separate scheme IDs are used to reduce confusion with our production environment.

Object identifiers

Object identifiers (OIDs) uniquely identify each object in the SEEK API. The ObjectIdentifier type in our schema mirrors the equivalent HR-JSON concept and looks something like this:
JSON
Copy
{
  "value": "globalPublicTest:candidate:uploaded:12uNaG7vqgW6G1jG7qp7ha"
}
You may notice that the object identifier is prefixed by a scheme identifier and appears to be mostly human readable. While it may be tempting to try to parse meaning out of the segments, they are an implementation detail. Object identifiers must be treated as opaque strings and stored in their entirety; do not disassemble or decode them.
There are a couple of benefits that these string identifiers have over traditional numeric identifiers:
  1. They are designed for forward compatibility.
    Small integer fields run the risk of overflowing; the numeric identifier used by our legacy application export integration has already exhausted much of the positive range for a signed 32-bit integer. Object identifiers require more expansive data types from the get go and are carefully namespaced. This allows us to extend the SEEK API to additional markets outside of Australia & New Zealand without making breaking changes to the identifiers.
  2. They are not easily guessable.
    A sequential numeric identifier makes it trivial to exploit insufficient access control with insecure direct object references . This also means that object identifiers are not ordered. Assumptions made around sequential ordering are often hard to meet when working with increasingly distributed and parallel systems. For scenarios such as pagination, the SEEK API opts to use opaque cursors.

Cursor-based pagination

The SEEK API adheres to the Relay GraphQL Cursor Connections Specification , which prescribes a standard way of expressing paginated queries and results. This includes bidirectional pagination, so you can request either the previous page or the next page of results from a given point:
QueryVariables
query($cursor: String!, $limit: Int!, $schemeId: String!) {
  # the two events that occurred just before the specified event
  justBefore: events(before: $cursor, last: $limit, schemeId: $schemeId) {
    edges {
      cursor
    }
  }
  # the two events that occurred just after the specified event
  justAfter: events(after: $cursor, first: $limit, schemeId: $schemeId) {
    edges {
      cursor
    }
  }
}
Cursors allow us to identify an exact point in the dataset to resume pagination from. By contrast, while offset-based pagination may seem more intuitive, the offsets will shift whenever a new item is added or removed from the dataset. This can lead to items being skipped or duplicated across pages, which is undesirable for critical workflows like retrieving pages of candidate applications.

Enums and code lists

While GraphQL’s native enumeration type works well with a frozen set of values, adding a new value to an existing enum can be a breaking change to certain client implementations.
The SEEK API prefers to represent non-frozen enumerations as GraphQL strings. These fields typically have a Code suffix and map to code lists defined by HR-JSON. The currently defined values are documented in their descriptions, but you should write your code to gracefully handle future additions.
For example, the PositionOpening object contains a statusCode field that includes the following in its description:
Currently, three codes are defined:
  • Incomplete indicates the position opening is in a draft state.
  • Active indicates the position opening is open.
  • Closed indicates the position opening has been closed.
The SEEK API also provides a code list field on each interface . You can use this code list field or the __typename meta field to trigger different branches in your code.
For example, the Event interface defines a typeCode field:
QueryVariables
query($id: String!) {
  # Returns an Event interface
  event(id: $id) {
    # Meta field indicates the concrete type
    __typename
    # Code list field indicates the concrete type
    typeCode
    # GraphQL fragment can select fields from the concrete type
    ... on PositionProfilePostedEvent {
      positionProfileId
    }
  }
}

Mutation conflicts

Mutations that create objects in the graph may not succeed due to unique constraints or idempotency. Such mutations may result in a success or conflict, collectively represented by a GraphQL union .
For example, the createWebhookSubscription mutation has the following return type:
GraphQL
union CreateWebhookSubscriptionPayload =
    CreateWebhookSubscriptionPayload_Conflict
  | CreateWebhookSubscriptionPayload_Success
Make sure to check the result of a mutation instead of assuming that it succeeded. You can use inline fragments to select fields from each union member. The conflict result allows you to identify and describe the existing object that conflicts with your operation.
The conflict result includes the existing object under a distinct field name that starts with conflicting. You can either check for the existence of this field or query the __typename meta field to trigger different branches in your code.
MutationVariables
mutation($input: CreateWebhookSubscriptionInput!) {
  createWebhookSubscription(input: $input) {
    # Meta field indicates the concrete type
    __typename
    ... on CreateWebhookSubscriptionPayload_Conflict {
      # Details of the existing webhook subscription that conflicts
      conflictingWebhookSubscription {
        id {
          value
        }
        createDateTime
      }
    }
    ... on CreateWebhookSubscriptionPayload_Success {
      # Details of the newly created webhook subscription
      webhookSubscription {
        id {
          value
        }
        createDateTime
      }
    }
  }
}