GraphQL API – Introduction and Usage

Using Aligni’s GraphQL API

Endpoint

Aligni’s GraphQL endpoint is:

https://ORGANIZATION.aligni.com/api/v3/graphql

You need to replace ORGANIZATION with your Aligni organization’s subdomain.

Authentication

Aligni supports personal API tokens. You need to use a different API token for each Aligni organization.

To create a new API token, visit your account settings page and then “API Tokens”. If you don’t already have an API token, you can create one by clicking on “Create new token”.

To authenticate your requests, pass the API token with the following request header:

Authorization: Token API_TOKEN

Here’s an example cURL call that uses an API token:

curl \
  -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Token oid3vLgynoy_Yl1gZkrgkLEq3J" \
  -d '{"query": "{ manufacturers { nodes { name id } } }"}' \
  https://demo.aligni.org/api/v3/graphql

Download Aligni GraphQL Schema

Do I Need the GraphQL Schema?

In most cases you will not need to download the full GraphQL schema. In development, some tools might need or at least benefit from having the entire schema.

In short, if you don’t know that you need this, you probably don’t.

The easiest way to download the whole Aligni GraphQL schema is to use a command line interface (CLI) tool, such as gql-cli (implemented in Python) or graphql-cli (implemented in JavaScript).

To download the schema with gql-cli, install GQL and execute the following command:

gql-cli https://demo.aligni.org/api/v3/graphql --print-schema > aligni-schema.graphql

Guides

To learn more about using the GraphQL API using Python or Ruby, visit the following guides:

Pagination

Aligni’s GraphQL API limits the number of items that you can fetch in a single request, to protect against excessive or abusive requests to servers. By default, the API will return 10 items when you request a collection of items. By supplying first or last, you can request fewer or more items. The maximum value is 100.

If the total number of items is higher than 100, the response is divided into “pages” of the specified size. These pages can be requested one at a time until you retrieve the whole data set.

Key Concepts

  • Connection: Represents a collection of data items, providing information about pagination such as total count and page boundaries.
  • Edge: Represents a single item in the connection, containing both the item (node) and a cursor for pagination.
  • Node: The actual data item within the edge.

Schema Definition

Here’s an example schema definition for a paginated query:

type Query {
  parts(
    after: String
    before: String
    first: Int
    last: Int
  ): PartConnection
}

type PartConnection {
  edges: [PartEdge]
  nodes: [Part]
  pageInfo: PageInfo
}

type PartEdge {
  node: Part
  cursor: String
}

type Part {
  id: ID!
  partNumber: String
  manufacturerPn: String
  ...
}

type PageInfo {
  endCursor: String
  startCursor: String
  hasNextPage: Boolean
  hasPreviousPage: Boolean
}

Query Parameters

  • after: The cursor after which to retrieve the next set of items.
  • before: The cursor before which to retrieve the previous set of items.
  • first: The number of items to retrieve starting from the after cursor. The default value is 10. The maximum value is 100.
  • last: The number of items to retrieve ending at the before cursor. The default value is 10. The maximum value is 100.

Response Structure

  • edges: An array of edges, each containing:
    • node: The actual data item.
    • cursor: A unique identifier for the item.
  • nodes: An array of nodes.
  • pageInfo: An object containing:
    • endCursor: The cursor of the last item in the current page.
    • startCursor: The cursor of the first item in the current page.
    • hasNextPage: A boolean indicating if there are more items to fetch.
    • hasPreviousPage: A boolean indicating if there are previous items to fetch.

Example Query

Fetching the first 10 parts using edges:

{
  parts(first: 10) {
    edges {
      node {
        id
        partNumber
        manufacturerPn
      }
      cursor
    }
    pageInfo {
      endCursor
      startCursor
      hasNextPage
      hasPreviousPage
    }
  }
}

An example response:

{
  "data": {
    "parts": {
      "edges": [
        … Part "nodes" 1-9 are included in this response…
        {
          "node": {
            "id": "itm_0113ZW52YG3186HQZTZZA87J0Z",
            "partNumber": "100012",
            "manufacturerPn": "LTST-C190GKT"
          },
          "cursor": "MTA"
        }
      ],
      "pageInfo": {
        "endCursor": "MTA",
        "startCursor": "MQ",
        "hasNextPage": true,
        "hasPreviousPage": false
      }
    }
  }
}

Each “edge” in the response has a “node” (part) and a “cursor”. The “pageInfo” response has an “endCursor” value. You can use that value to fetch the next page, using the end cursor as the “after” value:

{
  parts(first: 10, after: "MTA") {
    edges {
      node {
        id
        partNumber
        manufacturerPn
      }
      cursor
    }
    pageInfo {
      endCursor
      startCursor
      hasNextPage
      hasPreviousPage
    }
  }
}

Response:

{
  "data": {
    "parts": {
      "edges": [
        {
          "node": {
            "id": "itm_0113ZW52YG77AQ9BQW8DZF5ZB2",
            "partNumber": "100013",
            "manufacturerPn": "67068-0000"
          },
          "cursor": "MTE"
        },
        … Part "nodes" 2-10 are included in this response…
      ],
      "pageInfo": {
        "endCursor": "MjA",
        "startCursor": "MTE",
        "hasNextPage": true,
        "hasPreviousPage": true
      }
    }
  }
}

You can continue fetching new pages as long as “hasNextPage” is “true”.


Instead of using “edges”, you can fetch the first 10 parts using “nodes”, which won’t give you the cursor of each part:

{
  parts(first: 10) {
    nodes {
      id
      partNumber
      manufacturerPn
    }
    pageInfo {
      endCursor
      startCursor
      hasNextPage
      hasPreviousPage
    }
  }
}

Response:

{
  "data": {
    "parts": {
      "nodes": [
        … Part "nodes" 1-9 are included in this response…
        {
          "id": "itm_0113ZW52YG3186HQZTZZA87J0Z",
          "partNumber": "100012",
          "manufacturerPn": "LTST-C190GKT"
        }
      ],
      "pageInfo": {
        "endCursor": "MTA",
        "startCursor": "MQ",
        "hasNextPage": true,
        "hasPreviousPage": false
      }
    }
  }
}

Filtering

Filtering allows retrieving specific subsets of data from a GraphQL API based on defined criteria.

Key Concepts

  • Filter Input: Defines the structure of the input object used to specify filtering criteria.
  • Filter Operator: Specifies the type of comparison to be applied in the filtering operation (e.g., equals, contains, greater than, less than, etc.).
  • Filter Field: Refers to the field on which the filtering operation is applied.
  • Filter Argument: The argument used in the query to pass the filter input.

Schema Definition

Here’s an example schema definition for a filtered query:

type Query {
  parts(
    filters: [FiltersInput!]
  ): PartConnection
}

type FiltersInput {
  field: String!
  value: OperatorValueInput!
}

type OperatorValueInput {
  gt: OperatorScalar
  lt: OperatorScalar
  eq: OperatorScalar
  gte: OperatorScalar
  lte: OperatorScalar
  in: [OperatorScalar!]
  notIn: [OperatorScalar!]
  range: RangeInput
}

type RangeInput {
  min: RangeScalar!
  max: RangeScalar!
}

Filter Operators

  • Greater Than (gt): Matches values that are greater than the specified value.
  • Less Than (lt): Matches values that are less than the specified value.
  • Equals (eq): Matches the exact value of the field.
  • Greater Than or Equal (gte): Matches values that are greater than or equal to the specified value.
  • Less Than or Equal (lte): Matches values that are less than or equal to the specified value.
  • In (in): Matches values contained in the specified list.
  • Not In (notIn): Matches values not contained in the specified list.
  • Range (range): Matches values that are in the specified range.

All fields described in the filter need to be matched. That means that the filter merges all the conditions using a logical and operator.

Query Example

Here’s an example query that demonstrates how to use filtering:

query {
  parts(filters: [
    {field: "value", value: {gt: "50"}},
    {field: "value", value: {lt: "1000"}}
  ]) {
    nodes {
      manufacturerPn
      value
    }
  }
}

Example Response

{
  "data": {
    "parts": {
      "nodes": [
        {
          "manufacturerPn": "9C06031A1000JLHFT",
          "value": 100
        },
        {
          "manufacturerPn": "9C06031A3300JLHFT",
          "value": 330
        },
        {
          "manufacturerPn": "RC0603JR-07390RL",
          "value": 390
        }
      ]
    }
  }
}

Error Handling

In GraphQL, HTTP status codes work differently from REST API status codes. The most important difference is that the GraphQL API can return a “200 OK” response code in cases that would typically produce 4xx or 5xx errors in REST.

For that reason, it’s very important to query and check the “errors” part of each GraphQL response.

Errors in Queries

For example, if a user that doesn’t have the permission to see contacts tries to access contacts using the GraphQL API with the following query:

query Contacts {
  contacts {
    nodes {
      email
    }
  }
}

They will see the following response:

{
  "data": null,
  "errors": [
    {
      "message": "You do not have permission to perform the selected action.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "contacts"
      ]
    }
  ]
}

Note that “data” is “null” and there is “errors” array that includes all errors (in this case, there’s just one).

Errors in Mutations

When you execute a mutation, you need to define what the response will include. It’s advisable to always include “errors” in the response.

For example, the following mutation will create a contact, it will return the ID of the new contact if the request was successful and errors if it wasn’t:

mutation CreateContact {
  contactCreate(contactInput: {}) {
    contact {
      id
    }
    errors
  }
}

Always check if there are any errors in the response.

Rate Limiting

Rate limiting is a technique used to control the amount of incoming and outgoing traffic to and from a server. Nearly all API implementations employ rate limiting to help achieve several goals:

  • Improve security by mitigating denial-of-service (DoS) attacks and reducing the risk of brute-force attacks.
  • Ensure fair usage of limited server resources among all users.
  • Improve the users experience for all users by ensuring that the API remains responsive and available.

Complexity

Complexity is a measure of the computational cost of processing an API query or mutation. Aligni calculates complexity by assigning a score to each field and operation to determine the overall demand of a query. Each requested field in a query is valued at 1.

Additionally, certain segments of the API carry a multiplier that increases the cost of the query based on which area of operation the API is related to.

Note that complexity is subject to change without notice.

API SegmentQueryMutation
Default110
Item Master110
Inventory110
ActiveQuote220
Purchasing220
Build System5100
Demand System5100

Rate Limits

Rate limits establish a maximum velocity at which complexity may be accrued by all API clients within an organization. If a rate limit is exceeded, future requests will return a rate limit error response until the end of the measurement interval when the meter resets.

Complexity is computed as a measure of the database and server impact of a particular query or mutation. The limit and method for measuring this are subject to change without notice. The complexity of each query is returned with the results.

 StandardAPI Premium
Requests per Minute1030
Complexity Limit (per request)10,00020,000
Complexity Limit (per hour)600,0003,600,000
Maximum Pagination100100
Maximum Concurrent Requests12

The server performs a complexity estimate prior to actually performing the work of an API call. This estimate is based on the maximum expected complexity and could be much greater than the actual complexity in some cases.

Reducing Estimated Complexity

If the server does not know how much data will be returned, it uses the default page size to help with this computation. Therefore, in deeply-nested queries, the estimated complexity can quickly grow. You can mitigate this possibility (and, correspondingly, the possibility of a request being declined based on this estimate) by reducing the depth of queries or by explicitly specifying how many records you want.

The following query, for example, will assume the default page size of 10 to estimate the number of revisions in the query:

revisions(filters: {field: "active", value: {eq: true}})

The following query, on the other hand, specifies that only the first item will be returned, so the estimated complexity will be much smaller:

revisions(first: 1, filters: {field: "active", value: {eq: true}})

Another common example is related to part custom parameters. You can fetch the number of custom parameter fields using the following two queries (for non-revisioned and revisioned parameters, respectively:

partParameterFields(filters: {field: "revisioned", value: {eq: "false"}}) {
  totalCountNonRevisioned
}
partParameterFields(filters: {field: "revisioned", value: {eq: "true"}}) {
  totalCountRevisioned
}

Then, in a subsequent request for parts, you can refer to these counts to eliminate the ambiguity and make the complexity more definitive. (Note that the variable substitution will need to be performed into the query as appropriate.)

parts {
  nodes {
    manufacturerPn
    customParameters(first: #{totalCountNonRevisioned}) {
      nodes {
        name
        value
      }
    }
    revisions(first: 1, filters: {field: "active", value: {eq: "true"}}) {
      nodes {
        revisionName
        customParameters(first: #{totalCountRevisioned}) {
          nodes {
            name
            value
          }
        }
      }
    }
  }
}