Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

GraphQL is a syntax that describes how to ask for or manipulate data, and is generally used to load data from a server (eNVD) to a client (you), or for you to create or request changes to data.

...

In the existing REST API, retrieving a consignment and its forms and subforms would require multiple requests. Using GraphQL this process can be simplified to look more like the below, asking for all information in a single call. Note that there are inline comments to help explain what each field is:

Expand
titleUsing GraphQL
Code Block
languagegraphql
query {
  consignments {
		totalCount
    items {
      # The consignment number
      number
      # The forms attached to this consignment
      forms {
        # Program name e.g. LPAC1
        type
        # The form serial number
        serialNumber
      }
      # The url for the printed form
      pdfUrl
      # These are self-explantory meta fields that are pre-filled during creation/update
      submittedAt
      updatedAt
      updatedBy
      # The current status of the consignment
      status
      # The current species of the consignment
      species
      # These are the movement fields for all forms
      owner {
        address {
          line1
          postcode
          state
          town
        }
        name
        pic
      }
      destination {
        address {
          line1
          postcode
          state
          town
        }
        name
        pic
      }
      consignee {
        address {
          line1
          postcode
          state
          town
        }
        name
        pic
      }
      origin {
        address {
          line1
          postcode
          state
          town
        }
        name
        pic
      }
      # A global declaration across all forms in the consignment
      declaration {
        accept
        address {
          line1
          postcode
          state
          town
        }
        certificateNumber
        date
        email
        fullName
        phone
        signature
      }
      # The list of questions for the consignment based on the forms attached (this will be dynamic due to this)
      questions {
        # The question id, you will need ths in order to answer it
        id
        # The question text, i.e. the actual question itself
        text
        # The question help, a long text field in markdown to explain the question
        help
        # The type of the question. This will help in choosing how to diplay the question,
        # The type can be SINGLE_CHOICE, MULTIPLE_CHOICE, STRING, NUMBER etc
        type
        # If this question has a limited field of answers, this will contain how to display the
        # answer and what the value to send for it is
        # If this contains nothing, then the user can answer it with anything they want
        acceptableAnswers {
          displayName
          value
        }
        # This is a list of questions that are related to this one, can be n-levels deep
        # When there is no `trigger` defined, this means that the child question is always visible
        # When there is a `trigger` defined, this means that the child question is only visible when the condition passes
        # e.g. if the `trigger` is: `{ id: '1', value: 'Yes }`, this means that the question with id "1" must have a value of "Yes" for this to be visible
        # typically, that question will be the parent question which is containing this child question
        childQuestions {
          id
          text
          help
          type
          acceptableAnswers {
            displayName
            value
          }
          triggers {
            questionId
            value
          }
        }
      }
      # These are the answers to the questions presented above. The `questionId` will allow you to figure out what to insert as the current answer
      answers {
        # The question id 
        questionId
        # The value of the answer
        value
        # An index if this is part of an array to indicate position, otherwise null
        index
      }
    }
  }
}

A single consignment request is similar, and you would list fields the same as those in the items field above.

Code Block
# Or request a single consignment
query {
  consignment(id: "C-12341234") {
    ... same as above
  }
}

Notice now that there is only one (1) endpoint at /graphql which you can query for data from within Forms, Subforms and the URL for the printed Consignment.

Create a consignment

Previously, in the V3 API, there were multiple REST calls required in order to create a consignment and attach forms and subforms to it. Each of them had to be done individually in a sequence such as:

Code Block
POST /Auth: LPA oauth2 token request 
POST /Consignments: Create consignment
PUT /Forms: Create Data for Consignment form {LPA.C.1} 
POST /Subforms: Create consignment Subform values for Program LPA.C.1 {Quantity}
PUT /Subforms: Update the consignment Subform values for Program LPA.C.1 {Quantity}
POST /Subforms: Create consignment Subform values for Program LPA.C.1 {PartB}
PUT /Subforms: Update the consignment Subform values for Program LPA.C.1 {PartB}
PUT /Consignments: Update completed status to submitted
GET /Print: Print Consignment 

With the GraphQL API you now only need to make one call (called a mutation) as shown below. Note that questions and answers are now centralised, with the answers automatically mapped out to all relevant forms. You also have the opportunity to define the information that you want to receive in return for the successful call, which helps enable verifying changes are as expected and keeping in sync.

Code Block
languagegraphql
mutation { createOrSaveConsignment(input: { # The forms to attach to ths consignment forms: [LPAC1] # The initial movement date estimated for this consignment # The transporter movement date will be applied in answers movementDate: "2020-10-15" destination: { name: "Joe Bloggs" pic: "AAAAAAAA" } # The list of answers for any questions (partial or otherwise) answers: [ # This shows an example of a SINGLE_CHOICE question being answered with Yes { questionId: "1", index: null, value: "Yes" }

Expand
titleUsing C#
Code Block
languagec#
// NOTE: The full code of this example can be found in the appendix. Only the relevant snippet
// is included here due to the verbosity

public async Task<IEnumerable<Consignment>> QueryConsignments(GraphQLHttpClient client)
{

    // Query copied from document
    var request = new GraphQLRequest
    {
        Query = @"
        query {
            consignments {
                totalCount
                items {
                    # The consignment number
                    number
                    # The forms attached to this consignment
                    forms {
                        # Program name e.g. LPAC1
                        type
                        # The 
This
form 
show
serial 
an
number
example
 
of
 
the
 
`quantity`
 
subform
 
being
 
answered
 
a
 
an
 
array
       
#
 
as
 
can
 
be
 
seen
 
by
 
the
 
definition
 
of
serialNumber
the
 
`index`
 
parameter
       
{
 
questionId:
 
"2",
 
index:
 
1,
 
value:
 
"8"
 
}
    }
  
{
 
questionId:
 
"2",
 
index:
 
0,
 
value:
 
"4"
 
}
       
{
 
questionId:
 
"3",
 
index:
 
1,
# 
value: "2" }
The url for the printed form
   
{
 
questionId:
 
"3",
 
index:
 
0,
 
value:
 
"2"
 
}
       
{
 
questionId:
 
"4", index: 0, value: "breed1" }
 pdfUrl
          
{
 
questionId:
 
"4",
 
index:
 
1,
 
value:
 
"breed2"
 
}
   # These are self-explantory 
{
meta 
questionId: "5", index: 1, value: "Heifer : F" }
fields that are pre-filled during creation/update
          
{
 
questionId:
 
"5",
 
index:
 
0,
 
value:
 
"Bull
 
:
 
M"
 
}
 submittedAt
     
{
 
questionId:
 
"8",
 
index:
 
0,
 
value:
 
"Yes"
 
}
       
{
 
questionId: "8", index: 1, value: "Yes" }
updatedAt
            
{
 
questionId:
 
"9",
 
index:
 
0,
 
value:
 
"https://www.mla.com.au/globalassets/mla-corporate/mla_logo_home.png"
 
}
 updatedBy
     
{
 
questionId:
 
"9",
 
index:
 
1,
 
value: "https://www.mla.com.au/globalassets/mla-corporate/mla_logo_home.png" }
          # The current status of the consignment
                    status
     
]
   
})
 
{
     
data
 
{
     # The 
number
current species of the consignment
  
forms
 
{
         
type
        species
serialNumber
       
}
       
pdfUrl
     
}
 # These 
} }

Benefits

As can be seen in the above example, you won’t need to:

  • Create each form individually

  • Create each subform individually

  • Duplicate answers across forms and subforms

  • Use multiple endpoints to achieve this, only 1 call is required!

This helps reduce the number of calls which reduces network traffic and the time to process a query or change. Importantly, it also ensures alignment in the data across all forms.

Update a consignment

An update looks very similar to a create, where you provide what is essentially a patch of the information you wish to change again using a mutation. With partial updates you just provide the information you want to change, not a complete picture of how the data must be after the change.

Code Block
languagegraphql
mutation {
  createOrSaveConsignment(input: {
    number: "C-12345678"
    answers: [
      # Here we are updating the values for the `quantity subform`
      { questionId: "4", index: 0, value: "Hereford" }
    ]
  }) {
    # Same as create
    data {
      answers {
        index
        questionId
        value
      }
    }
  }
}

Printing a consignment

In a consignment query or mutation there is a field pdfUrl which, if requested, contains a URL which can be used to print the consignment.

Mapping from Forms to Questions

As seen in the above examples, part of the move from the REST API to the GraphQL API is understanding how the new format of questions and answers maps back to forms and subforms. To help navigate this mapping, the attached json file contains mappings between the two.

View file
namegraphql-api-form-mapping.json

It contains an array of entries such as:

Code Block
  {
    "Id": "1",
    "Text": "Please provide a description of the livestock moving",
    "Form": "LPAC1",
    "Field": "description.quantity"
  },

In this entry there are 3 pieces of information to help you find the form and subform based piece of information, such as how it would referred to using the REST API:

  • “Text” - The question text that is presented to the user. This helps to double check that the question is the right one, and provides text they can choose to use in their system.

  • “Form”- The form that the question links to. There are separate entries in the JSON for each form, so if a question is used for multiple forms (which is often the case) then it will appear multiple times, once for each form. Note that form names have had their periods removed, so LPA.C.1 will be LPAC1 in the JSON.

  • “Field” - This points to the JSON schema field in the form model, and is the way that integrators can map between the old model using this field and ‘Form’, and the new model which uses ‘ID’

As a further example / explanation of the “Field”, given the following excerpt from the MSA form model:

Code Block
{
  "version": "0.1",
  "showTitle": false,
  "type": "object",
  "format": "IsValidDateOfDispatch,IsValidDeclarationDate",
  "properties": {
    "owner": {
      "type": "object",
      "properties": {
        "msa-reg-num": {
          "title": "MSA Registration no. of owner",
          "type": "string",
          "maxLength": 4
        },
        "pic": {
          "title": "Property Identification Code (PIC) of owner",
          "dictionaryKey": "owner",
          "type": "string",
          "maxLength": 8,
          "format": "PIC_IsValid"
        }
      },

the MSA Registration no. of owner would be identified as:

Code Block
owner.msa-reg-num

The final piece of information is to help you map this to the new question and answer format used in the GraphQL API:

  • “ID” - This is the ID of the question in its new structure, and will become the new way that integrators refer to a question instead of their json path

FAQ

Are there limits as to how much I can request in one call?

We trust that consumers will operate a fair use manner, only requesting the data they require at the rate which they require it. An example of this would be a query for the 20 most recently modified consignments for a list. For such a scenario it is generally reasonable to request just the high level information for each consignment, leaving the detail for future calls.

How are errors handled?

A successful request returns a status of 200, however unlike a REST API call, that does not mean your query or mutation was successful. Instead you need to look in the body of the response and check if any errors were returned.

Code Block
{
  ...
  "errors": [
    {
      "message": "",
      "path": ""
    }
  ]
}

See https://graphql.org/learn/serving-over-http/#response for more detail.

How is the performance compared to the REST API?

The performance is excellent, however it does depend on what is requested in some ways that can be appreciated. If you are requesting a large amount of data and the resolution takes a while then that single GraphQL query may take some time to return. Given however that this is achievable in a single call with generally no further calls required, the result is greatly improved overall performance.

How can I request form data if we’re no longer submitting the data as forms?

The change to store and manage a consignment a set of questions and answers abstracted away from forms generally removes the need for querying the data in the structure of a particular form. Having said that, you can, if you wish, query for the data for a specific form and the API will provide that to you, mapping from the consignment and its questions and answers format to the form in question. From a consumer’s perspective this means that your queries and mutations in general are vastly simplified, and for the rare case when the data is required in a specific form’s shape, it is possible.

Do we need to use the questions, help, hint and other text you provide within our systems?

It is advised to use this information to provide users with a unified experience and to ensure that the wording surrounding a question stays aligned with how the question is used and what it means. Moving away from the usage of these fields puts the onus on API consumers to monitor them for updates and keep the wording in their implementation aligned.

When I’m making calls in Playground it is saying that I am not authenticated, how can I fix this?

...

are the movement fields for all forms
                    owner {
                        address {
                            line1
                            postcode
                            state
                            town
                        }
                        name
                        pic
                    }
                    destination {
                        address {
                            line1
                            postcode
                            state
                            town
                        }
                        name
                        pic
                    }
                    consignee {
                        address {
                            line1
                            postcode
                            state
                            town
                        }
                        name
                        pic
                    }
                    origin {
                        address {
                            line1
                            postcode
                            state
                            town
                        }
                        name
                        pic
                    }
                    # A global declaration across all forms in the consignment
                    declaration {
                        accept
                        address {
                            line1
                            postcode
                            state
                            town
                        }
                        certificateNumber
                        date
                        email
                        fullName
                        phone
                        signature
                    }
                    # The list of questions for the consignment based on the forms attached (this will be dynamic due to this)
                    questions {
                        # The question id, you will need ths in order to answer it
                        id
                        # The question text, i.e. the actual question itself
                        text
                        # The question help, a long text field in markdown to explain the question
                        help
                        # The type of the question. This will help in choosing how to diplay the question,
                        # The type can be SINGLE_CHOICE, MULTIPLE_CHOICE, STRING, NUMBER etc
                        type
                        # If this question has a limited field of answers, this will contain how to display the
                        # answer and what the value to send for it is
                        # If this contains nothing, then the user can answer it with anything they want
                        acceptableAnswers {
                            displayName
                            value
                        }
                        # This is a list of questions that are related to this one, can be n-levels deep
                        # When there is no `trigger` defined, this means that the child question is always visible
                        # When there is a `trigger` defined, this means that the child question is only visible when the condition passes
                        # typically, that question will be the parent question which is containing this child question
                        childQuestions {
                            id
                            text
                            help
                            type
                            acceptableAnswers {
                                displayName
                                value
                            }
                            triggers {
                                questionId
                                value
                            }
                        }
                    }
                    # These are the answers to the questions presented above. The `questionId` will allow you to figure out what to insert as the current answer
                    answers {
                        # The question id 
                        questionId
                        # The value of the answer
                        value
                        # An index if this is part of an array to indicate position, otherwise null
                        index
                    }
                }
            }
        }
        "
    };

    var response = await client.SendQueryAsync<ConsignmentListResponseType>(request);

    // Now you can use the data from the response 
    return response.Data.Consignments.Items;
}

A single consignment request is similar, and you would list fields the same as those in the items field above.

Expand
titleUsing GraphQL
Code Block
# Or request a single consignment
query {
  consignment(id: "C-12341234") {
    ... same as above
  }
}

Expand
Code Block
languagec#
// NOTE: The full code of this example can be found in the appendix. Only the relevant snippet
// is included here due to the verbosity

public async Task<Consignment> QueryConsignment(GraphQLHttpClient client, string number)
{
    // You can query for everything as per the above but for this example we only care about the number
    var request = new GraphQLRequest
    {
        Query = @"
        query QueryConsignment ($id: String!) {
            consignment(id: $id) {
                number
            }
        }
        ",
        OperationName = "QueryConsignment",
        Variables = new
        {
            id = number
        }
    };

    var response = await client.SendQueryAsync<ConsignmentResponseType>(request);
    return response.Data.Consignment;
}

Notice now that there is only one (1) endpoint at /graphql which you can query for data from within Forms, Subforms and the URL for the printed Consignment.

Create a consignment

Previously, in the V3 API, there were multiple REST calls required in order to create a consignment and attach forms and subforms to it. Each of them had to be done individually in a sequence such as:

Code Block
POST /Auth: LPA oauth2 token request 
POST /Consignments: Create consignment
PUT /Forms: Create Data for Consignment form {LPA.C.1} 
POST /Subforms: Create consignment Subform values for Program LPA.C.1 {Quantity}
PUT /Subforms: Update the consignment Subform values for Program LPA.C.1 {Quantity}
POST /Subforms: Create consignment Subform values for Program LPA.C.1 {PartB}
PUT /Subforms: Update the consignment Subform values for Program LPA.C.1 {PartB}
PUT /Consignments: Update completed status to submitted
GET /Print: Print Consignment 

With the GraphQL API you now only need to make one call (called a mutation) as shown below. Note that questions and answers are now centralised, with the answers automatically mapped out to all relevant forms. You also have the opportunity to define the information that you want to receive in return for the successful call, which helps enable verifying changes are as expected and keeping in sync.

Expand
titleUsing GraphQL
Code Block
languagegraphql
mutation {
  createOrSaveConsignment(input: {
    # The forms to attach to ths consignment
    forms: [LPAC1]
    # The initial movement date estimated for this consignment
    # The transporter movement date will be applied in answers
    movementDate: "2020-10-15"
    destination: {
      name: "Joe Bloggs"
      pic: "AAAAAAAA"
    }
    # The list of answers for any questions (partial or otherwise)
    answers: [
      # This shows an example of a SINGLE_CHOICE question being answered with Yes
      { questionId: "17", index: null, value: "Yes" }
      
      # This show an example of the `quantity` subform being answered a an array
      # as can be seen by the definition of the `index` parameter
      { questionId: "2", index: 1, value: "8" }
      { questionId: "2", index: 0, value: "4" }
      { questionId: "3", index: 1, value: "2" }
      { questionId: "3", index: 0, value: "2" }
      { questionId: "4", index: 0, value: "breed1" }
      { questionId: "4", index: 1, value: "breed2" }
      { questionId: "5", index: 1, value: "Heifer : F" }
      { questionId: "5", index: 0, value: "Bull : M" }
      { questionId: "8", index: 0, value: "Yes" }
      { questionId: "8", index: 1, value: "Yes" }
      { questionId: "9", index: 0, value: "https://www.mla.com.au/globalassets/mla-corporate/mla_logo_home.png" }
      { questionId: "9", index: 1, value: "https://www.mla.com.au/globalassets/mla-corporate/mla_logo_home.png" }
    ]
  }) {
    data {
      number
      forms {
        type
        serialNumber
      }
      pdfUrl
    }
  }
}
Expand
titleUsing C#
Code Block
languagec#
// NOTE: The full code of this example can be found in the appendix. Only the relevant snippet
// is included here due to the verbosity

public async Task<Consignment> CreateConsignment(GraphQLHttpClient client)
{
    var request = new GraphQLRequest
    {
        Query = @"
        mutation CreateConsignment($input: CreateOrSaveConsignmentInput!) {
            createOrSaveConsignment(input: $input) {
                data {
                    number
                    createdAt
                    forms {
                        type
                        serialNumber
                    }
                    pdfUrl
                    answers {
                        questionId
                        index
                        value
                    }
                }
            }
        }
        ",
        OperationName = "CreateConsignment",
        Variables = new
        {
            input = new
            {
                // The forms to attach to ths consignment
                forms = new[] { "LPAC1" },
                // The initial movement date estimated for this consignment
                // The transporter movement date will be applied in answers
                movementDate = "2020-10-15",
                destination = new
                {
                    name = "Joe Bloggs",
                    pic = "AAAAAAAA",
                },
                // The list of answers for any questions (partial or otherwise)
                answers = new[] {
                    // This shows an example of a SINGLE_CHOICE question being answered with Yes
                    new AnswerType { QuestionId = "17", Index = null, Value = "Yes" },

                    // This show an example of the `quantity` subform being answered a an array
                    // as can be seen by the definition of the `index` parameter
                    new AnswerType{ QuestionId = "2", Index = 1, Value = "8" },
                    new AnswerType{ QuestionId = "2", Index = 0, Value = "4" },
                    new AnswerType{ QuestionId = "3", Index = 1, Value = "2" },
                    new AnswerType{ QuestionId = "3", Index = 0, Value = "2" },
                    new AnswerType{ QuestionId = "4", Index = 0, Value = "breed1" },
                    new AnswerType{ QuestionId = "4", Index = 1, Value = "breed2" },
                    new AnswerType{ QuestionId = "5", Index = 1, Value = "Heifer : F" },
                    new AnswerType{ QuestionId = "5", Index = 0, Value = "Bull : M" },
                    new AnswerType{ QuestionId = "8", Index = 0, Value = "Yes" },
                    new AnswerType{ QuestionId = "8", Index = 1, Value = "Yes" },
                    new AnswerType{ QuestionId = "9", Index = 0, Value = "https://www.mla.com.au/globalassets/mla-corporate/mla_logo_home.png" },
                    new AnswerType{ QuestionId = "9", Index = 1, Value = "https://www.mla.com.au/globalassets/mla-corporate/mla_logo_home.png" },
                },
            }
        }
    };

    var response = await client.SendQueryAsync<CreateOrSaveConsignmentResponseType>(request);
    return response.Data.CreateOrSaveConsignment.Data;
}

Benefits

As can be seen in the above example, you won’t need to:

  • Create each form individually

  • Create each subform individually

  • Duplicate answers across forms and subforms

  • Use multiple endpoints to achieve this, only 1 call is required!

This helps reduce the number of calls which reduces network traffic and the time to process a query or change. Importantly, it also ensures alignment in the data across all forms.

Update a consignment

An update looks very similar to a create, where you provide what is essentially a patch of the information you wish to change again using a mutation. With partial updates you just provide the information you want to change, not a complete picture of how the data must be after the change.

Expand
titleUsing GraphQL
Code Block
languagegraphql
mutation {
  createOrSaveConsignment(input: {
    number: "C-12345678"
    answers: [
      # Here we are updating the values for the `quantity subform`
      { questionId: "4", index: 0, value: "Hereford" }
    ]
  }) {
    # Same as create
    data {
      answers {
        index
        questionId
        value
      }
    }
  }
}
Expand
titleUsing C#
Code Block
languagec#
// NOTE: The full code of this example can be found in the appendix. Only the relevant snippet
// is included here due to the verbosity

public async Task<Consignment> UpdateConsignment(GraphQLHttpClient client, string number)
{
    var request = new GraphQLRequest
    {
        Query = @"
        mutation UpdateConsignment($input: CreateOrSaveConsignmentInput!) {
            createOrSaveConsignment(input: $input) {
                data {
                    number
                    answers {
                        index
                        questionId
                        value
                    }
                }
            }
        }
        ",
        OperationName = "UpdateConsignment",
        Variables = new
        {
            input = new
            {
                number = number,
                // Since it is possible to delete at an index inside an array
                // All values for the array must be sent to cover against potential deletion of an index
                answers = new[] {

                    new AnswerType{ QuestionId = "2", Index = 1, Value = "8" },
                    new AnswerType{ QuestionId = "2", Index = 0, Value = "4" },
                    new AnswerType{ QuestionId = "3", Index = 1, Value = "2" },
                    new AnswerType{ QuestionId = "3", Index = 0, Value = "2" },
                    // This is the update
                    new AnswerType{ QuestionId = "4", Index = 0, Value = "Hereford" },
                    new AnswerType{ QuestionId = "4", Index = 1, Value = "breed2" },
                    new AnswerType{ QuestionId = "5", Index = 1, Value = "Heifer : F" },
                    new AnswerType{ QuestionId = "5", Index = 0, Value = "Bull : M" },
                    new AnswerType{ QuestionId = "8", Index = 0, Value = "Yes" },
                    new AnswerType{ QuestionId = "8", Index = 1, Value = "Yes" },
                    new AnswerType{ QuestionId = "9", Index = 0, Value = "https://www.mla.com.au/globalassets/mla-corporate/mla_logo_home.png" },
                    new AnswerType{ QuestionId = "9", Index = 1, Value = "https://www.mla.com.au/globalassets/mla-corporate/mla_logo_home.png" },
                }
            }
        }
    };

    var response = await client.SendQueryAsync<CreateOrSaveConsignmentResponseType>(request);
    // Here you'll see, '17' is still present as it is not an array field thus doesn't need to be handled differently
    return response.Data.CreateOrSaveConsignment.Data;
}

Printing a consignment

In a consignment query or mutation there is a field pdfUrl which, if requested, contains a URL which can be used to print the consignment.

Mapping from Forms to Questions

As seen in the above examples, part of the move from the REST API to the GraphQL API is understanding how the new format of questions and answers maps back to forms and subforms. To help navigate this mapping, the attached json file contains mappings between the two.

View file
namegraphql-api-form-mapping.json
pageDeveloper documentation
spaceE

It contains an array of entries such as:

Code Block
  {
    "Id": "1",
    "Text": "Please provide a description of the livestock moving",
    "Form": "LPAC1",
    "Field": "description.quantity"
  },

In this entry there are 3 pieces of information to help you find the form and subform based piece of information, such as how it would referred to using the REST API:

  • “Text” - The question text that is presented to the user. This helps to double check that the question is the right one, and provides text they can choose to use in their system.

  • “Form”- The form that the question links to. There are separate entries in the JSON for each form, so if a question is used for multiple forms (which is often the case) then it will appear multiple times, once for each form. Note that form names have had their periods removed, so LPA.C.1 will be LPAC1 in the JSON.

  • “Field” - This points to the JSON schema field in the form model, and is the way that integrators can map between the old model using this field and ‘Form’, and the new model which uses ‘ID’

As a further example / explanation of the “Field”, given the following excerpt from the MSA form model:

Code Block
{
  "version": "0.1",
  "showTitle": false,
  "type": "object",
  "format": "IsValidDateOfDispatch,IsValidDeclarationDate",
  "properties": {
    "owner": {
      "type": "object",
      "properties": {
        "msa-reg-num": {
          "title": "MSA Registration no. of owner",
          "type": "string",
          "maxLength": 4
        },
        "pic": {
          "title": "Property Identification Code (PIC) of owner",
          "dictionaryKey": "owner",
          "type": "string",
          "maxLength": 8,
          "format": "PIC_IsValid"
        }
      },

the MSA Registration no. of owner would be identified as:

Code Block
owner.msa-reg-num

The final piece of information is to help you map this to the new question and answer format used in the GraphQL API:

  • “ID” - This is the ID of the question in its new structure, and will become the new way that integrators refer to a question instead of their json path

FAQ

Are there limits as to how much I can request in one call?

We trust that consumers will operate a fair use manner, only requesting the data they require at the rate which they require it. An example of this would be a query for the 20 most recently modified consignments for a list. For such a scenario it is generally reasonable to request just the high level information for each consignment, leaving the detail for future calls.

How are errors handled?

A successful request returns a status of 200, however unlike a REST API call, that does not mean your query or mutation was successful. Instead you need to look in the body of the response and check if any errors were returned.

Code Block
{
  ...
  "errors": [
    {
      "message": "",
      "path": ""
    }
  ]
}

See https://graphql.org/learn/serving-over-http/#response for more detail.

How is the performance compared to the REST API?

The performance is excellent, however it does depend on what is requested in some ways that can be appreciated. If you are requesting a large amount of data and the resolution takes a while then that single GraphQL query may take some time to return. Given however that this is achievable in a single call with generally no further calls required, the result is greatly improved overall performance.

How can I request form data if we’re no longer submitting the data as forms?

The change to store and manage a consignment a set of questions and answers abstracted away from forms generally removes the need for querying the data in the structure of a particular form. Having said that, you can, if you wish, query for the data for a specific form and the API will provide that to you, mapping from the consignment and its questions and answers format to the form in question. From a consumer’s perspective this means that your queries and mutations in general are vastly simplified, and for the rare case when the data is required in a specific form’s shape, it is possible.

Do we need to use the questions, help, hint and other text you provide within our systems?

It is advised to use this information to provide users with a unified experience and to ensure that the wording surrounding a question stays aligned with how the question is used and what it means. Moving away from the usage of these fields puts the onus on API consumers to monitor them for updates and keep the wording in their implementation aligned.

When I’m making calls in Playground it is saying that I am not authenticated, how can I fix this?

Playground provides an interface to make queries to the GraphQL API, much like tools such as Insomnia and Postman. To make calls to an authenticated endpoint you must provide authentication headers much as you would for a call using any other tool. Within Playground there is a section where you can enter headers in JSON format, located in the version at the time of writing at the bottom of the screen. It is here that you need to correctly construct the Authorization header that is required along with a valid token, and Playground will pass this along with your calls which will then succeed.

Appendix

Expand
titleFull C# Script Example
Code Block
languagec#
#!/usr/bin/env dotnet-script

#r "nuget: GraphQL.Client, 3.2.2"
#r "nuget: GraphQL.Client.Serializer.Newtonsoft, 3.2.2"

// Using https://github.com/graphql-dotnet/graphql-client as the package of choice for this example

using System;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using GraphQL;
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.Newtonsoft;

#region DTOs
public enum EQuestionAnswer
{
    STRING,
    BOOLEAN,
    NUMBER,
    SINGLE_CHOICE,
    MULTIPLE_CHOICE,
    FILE,
    IMAGE,
    DATE,
    DATE_TIME,
    REPEATABLE,
    DISPLAY,
    DATE_MONTH,
}

public enum EForm
{
    LPAC0,
    LPAC1,
    EUC0,
    EUC1,
    HSC0,
    HSC1,
    MSAC0,
    MSAC1,
    NFASC0,
    NFASDDC0,
    NFASDDC1,
    NFASEUC0,
    NFASEUC1,
    LPABC0,
    LPABC1,
    LPASL0,
    LPASL1,
    HSSL0,
    HSSL1,
    LPAG0,
    G0517,
    LPAG2,
    HSG0
}

public class AddressType
{
    public string Line1 { get; set; }
    public string Town { get; set; }
    public string State { get; set; }
    public string Postcode { get; set; }
}

public class DeclarationType
{
    public bool Accept { get; set; }
    public string FullName { get; set; }
    public string Phone { get; set; }
    public DateTimeOffset Date { get; set; }
    public string Signature { get; set; }
    public AddressType Address { get; set; }
    public string Email { get; set; }
    public string CertificateNumber { get; set; }
}

public class EntityType
{
    public string Name { get; set; }
    public string PIC { get; set; }
    public AddressType Address { get; set; }
}

public class QuestionValidatorType
{
    public string ClassName { get; set; }
    public string Meta { get; set; }
    public string ErrorMessage { get; set; }
}

public class QuestionTriggerType
{
    public string Id { get; set; }
    public string JsonPath { get; set; }
    public string Value { get; set; }
}

public class AcceptableAnswerType
{

    public string DisplayName { get; set; }
    public string Value { get; set; }
    public IEnumerable<EForm> Forms { get; set; }
}

public class AnswerType
{
    public string QuestionId { get; set; }
    public string Value { get; set; }
    public double? Index { get; set; }
}

public class QuestionType
{

    public string Id { get; set; }
    public int Order { get; set; }
    public string Text { get; set; }
    public string Tip { get; set; }
    public string Help { get; set; }
    public EQuestionAnswer Type { get; set; }

    public IEnumerable<AcceptableAnswerType> AcceptableAnswers { get; set; }

    public IEnumerable<EForm> Forms { get; set; }

    public bool Deprecated { get; set; }
    public bool ReadOnly { get; set; }
    public IEnumerable<QuestionValidatorType> Validators { get; set; }

    public int Version { get; set; }

    public IEnumerable<QuestionType> ChildQuestions { get; set; }
    public IEnumerable<QuestionTriggerType> Triggers { get; set; }
}

public class ConsignmentType
{
    public string Number { get; set; }

    public EntityType Origin { get; set; }
    public EntityType Destination { get; set; }
    public EntityType Owner { get; set; }
    public EntityType Consignee { get; set; }

    public int Heads { get; set; }
    public string PdfUrl { get; set; }
    public string Status { get; set; }
    public string Species { get; set; }

    public DateTimeOffset? MovementDate { get; set; }
    public DateTimeOffset? MovementTime { get; set; }

    public IEnumerable<AnswerType> Answers { get; set; }
    public IEnumerable<QuestionType> Questions { get; set; }

    public DateTimeOffset CreatedAt { get; set; }
    public DateTimeOffset? UpdatedAt { get; set; }
    public string CreatedBy { get; set; }
    public string UpdatedBy { get; set; }

    public DateTimeOffset? SubmittedAt { get; set; }
    public string SubmittedBy { get; set; }

    public DeclarationType Declaration { get; set; }
    
}

#endregion

#region Response Definitions

public class ConsignmentListConnectionType
{
    public IEnumerable<ConsignmentType> Items { get; set; }
}

public class ConsignmentListResponseType
{
    public ConsignmentListConnectionType Consignments { get; set; }
}

public class ConsignmentResponseType
{
    public ConsignmentType Consignment;
}

public class CreateOrSaveConsignmentType
{
    public ConsignmentType Data;
}

public class CreateOrSaveConsignmentResponseType
{
    public CreateOrSaveConsignmentType CreateOrSaveConsignment { get; set; }
}

public class ConsignmentDeleteType
{
    public Boolean Success { get; set; }
}

public class ConsignmentDeleteResponseType
{
    public ConsignmentDeleteType DeleteConsignment { get; set; }
}

#endregion

#region Constants

const string TOKEN = "FILL-ME-IN";

const string GraphQLEndpoint = "https://graph.envd.uat.integritysystems.com.au/graphql";

#endregion

#region GraphQL Interactions
public async Task<IEnumerable<ConsignmentType>> QueryConsignments(GraphQLHttpClient client)
{

    // Query copied from document
    var request = new GraphQLRequest
    {
        Query = @"
        query {
            consignments {
                totalCount
                items {
                    # The consignment number
                    number
                    # The forms attached to this consignment
                    forms {
                        # Program name e.g. LPAC1
                        type
                        # The form serial number
                        serialNumber
                    }
                    # The url for the printed form
                    pdfUrl
                    # These are self-explantory meta fields that are pre-filled during creation/update
                    submittedAt
                    updatedAt
                    updatedBy
                    # The current status of the consignment
                    status
                    # The current species of the consignment
                    species
                    # These are the movement fields for all forms
                    owner {
                        address {
                            line1
                            postcode
                            state
                            town
                        }
                        name
                        pic
                    }
                    destination {
                        address {
                            line1
                            postcode
                            state
                            town
                        }
                        name
                        pic
                    }
                    consignee {
                        address {
                            line1
                            postcode
                            state
                            town
                        }
                        name
                        pic
                    }
                    origin {
                        address {
                            line1
                            postcode
                            state
                            town
                        }
                        name
                        pic
                    }
                    # A global declaration across all forms in the consignment
                    declaration {
                        accept
                        address {
                            line1
                            postcode
                            state
                            town
                        }
                        certificateNumber
                        date
                        email
                        fullName
                        phone
                        signature
                    }
                    # The list of questions for the consignment based on the forms attached (this will be dynamic due to this)
                    questions {
                        # The question id, you will need ths in order to answer it
                        id
                        # The question text, i.e. the actual question itself
                        text
                        # The question help, a long text field in markdown to explain the question
                        help
                        # The type of the question. This will help in choosing how to diplay the question,
                        # The type can be SINGLE_CHOICE, MULTIPLE_CHOICE, STRING, NUMBER etc
                        type
                        # If this question has a limited field of answers, this will contain how to display the
                        # answer and what the value to send for it is
                        # If this contains nothing, then the user can answer it with anything they want
                        acceptableAnswers {
                            displayName
                            value
                        }
                        # This is a list of questions that are related to this one, can be n-levels deep
                        # When there is no `trigger` defined, this means that the child question is always visible
                        # When there is a `trigger` defined, this means that the child question is only visible when the condition passes
                        # typically, that question will be the parent question which is containing this child question
                        childQuestions {
                            id
                            text
                            help
                            type
                            acceptableAnswers {
                                displayName
                                value
                            }
                            triggers {
                                questionId
                                value
                            }
                        }
                    }
                    # These are the answers to the questions presented above. The `questionId` will allow you to figure out what to insert as the current answer
                    answers {
                        # The question id 
                        questionId
                        # The value of the answer
                        value
                        # An index if this is part of an array to indicate position, otherwise null
                        index
                    }
                }
            }
        }
        "
    };

    var response = await client.SendQueryAsync<ConsignmentListResponseType>(request);

    // Now you can use the data from the response 
    return response.Data.Consignments.Items;
}

public async Task<ConsignmentType> QueryConsignment(GraphQLHttpClient client, string number)
{
    // You can query for everything as per the above but for this example we only care about the number
    var request = new GraphQLRequest
    {
        Query = @"
        query QueryConsignment ($id: String!) {
            consignment(id: $id) {
                number
            }
        }
        ",
        OperationName = "QueryConsignment",
        Variables = new
        {
            id = number
        }
    };

    var response = await client.SendQueryAsync<ConsignmentResponseType>(request);
    return response.Data.Consignment;
}

public async Task<ConsignmentType> CreateConsignment(GraphQLHttpClient client)
{
    var request = new GraphQLRequest
    {
        Query = @"
        mutation CreateConsignment($input: CreateOrSaveConsignmentInput!) {
            createOrSaveConsignment(input: $input) {
                data {
                    number
                    createdAt
                    forms {
                        type
                        serialNumber
                    }
                    pdfUrl
                    answers {
                        questionId
                        index
                        value
                    }
                }
            }
        }
        ",
        OperationName = "CreateConsignment",
        Variables = new
        {
            input = new
            {
                // The forms to attach to ths consignment
                forms = new[] { "LPAC1" },
                // The initial movement date estimated for this consignment
                // The transporter movement date will be applied in answers
                movementDate = "2020-10-15",
                destination = new
                {
                    name = "Joe Bloggs",
                    pic = "AAAAAAAA",
                },
                // The list of answers for any questions (partial or otherwise)
                answers = new[] {
                    // This shows an example of a SINGLE_CHOICE question being answered with Yes
                    new AnswerType { QuestionId = "17", Index = null, Value = "Yes" },

                    // This show an example of the `quantity` subform being answered a an array
                    // as can be seen by the definition of the `index` parameter
                    new AnswerType{ QuestionId = "2", Index = 1, Value = "8" },
                    new AnswerType{ QuestionId = "2", Index = 0, Value = "4" },
                    new AnswerType{ QuestionId = "3", Index = 1, Value = "2" },
                    new AnswerType{ QuestionId = "3", Index = 0, Value = "2" },
                    new AnswerType{ QuestionId = "4", Index = 0, Value = "breed1" },
                    new AnswerType{ QuestionId = "4", Index = 1, Value = "breed2" },
                    new AnswerType{ QuestionId = "5", Index = 1, Value = "Heifer : F" },
                    new AnswerType{ QuestionId = "5", Index = 0, Value = "Bull : M" },
                    new AnswerType{ QuestionId = "8", Index = 0, Value = "Yes" },
                    new AnswerType{ QuestionId = "8", Index = 1, Value = "Yes" },
                    new AnswerType{ QuestionId = "9", Index = 0, Value = "https://www.mla.com.au/globalassets/mla-corporate/mla_logo_home.png" },
                    new AnswerType{ QuestionId = "9", Index = 1, Value = "https://www.mla.com.au/globalassets/mla-corporate/mla_logo_home.png" },
                },
            }
        }
    };

    var response = await client.SendQueryAsync<CreateOrSaveConsignmentResponseType>(request);
    return response.Data.CreateOrSaveConsignment.Data;
}

public async Task<ConsignmentType> UpdateConsignment(GraphQLHttpClient client, string number)
{
    var request = new GraphQLRequest
    {
        Query = @"
        mutation UpdateConsignment($input: CreateOrSaveConsignmentInput!) {
            createOrSaveConsignment(input: $input) {
                data {
                    number
                    answers {
                        index
                        questionId
                        value
                    }
                }
            }
        }
        ",
        OperationName = "UpdateConsignment",
        Variables = new
        {
            input = new
            {
                number = number,
                // Since it is possible to delete at an index inside an array
                // All values for the array must be sent to cover against potential deletion of an index
                answers = new[] {

                    new AnswerType{ QuestionId = "2", Index = 1, Value = "8" },
                    new AnswerType{ QuestionId = "2", Index = 0, Value = "4" },
                    new AnswerType{ QuestionId = "3", Index = 1, Value = "2" },
                    new AnswerType{ QuestionId = "3", Index = 0, Value = "2" },
                    // This is the update
                    new AnswerType{ QuestionId = "4", Index = 0, Value = "Hereford" },
                    new AnswerType{ QuestionId = "4", Index = 1, Value = "breed2" },
                    new AnswerType{ QuestionId = "5", Index = 1, Value = "Heifer : F" },
                    new AnswerType{ QuestionId = "5", Index = 0, Value = "Bull : M" },
                    new AnswerType{ QuestionId = "8", Index = 0, Value = "Yes" },
                    new AnswerType{ QuestionId = "8", Index = 1, Value = "Yes" },
                    new AnswerType{ QuestionId = "9", Index = 0, Value = "https://www.mla.com.au/globalassets/mla-corporate/mla_logo_home.png" },
                    new AnswerType{ QuestionId = "9", Index = 1, Value = "https://www.mla.com.au/globalassets/mla-corporate/mla_logo_home.png" },
                }
            }
        }
    };

    var response = await client.SendQueryAsync<CreateOrSaveConsignmentResponseType>(request);
    // Here you'll see, '17' is still present as it is not an array field thus doesn't need to be handled differently
    return response.Data.CreateOrSaveConsignment.Data;
}

public async Task<Boolean> DeleteConsignment(GraphQLHttpClient client, string number)
{
    var request = new GraphQLRequest
    {
        Query = @"
        mutation DeleteConsignment($id: String!) {
            deleteConsignment(input: {
                id: $id
            }) {
                success
            }
        }
        ",
        OperationName = "DeleteConsignment",
        Variables = new
        {
            id = number
        }
    };

    var response = await client.SendQueryAsync<ConsignmentDeleteResponseType>(request);
    return response.Data.DeleteConsignment.Success;
}

#endregion

public async Task RunAll()
{
    // NOTE: This is purely for examples, do not treat what is here as production quality code.
    using var client = new GraphQLHttpClient(GraphQLEndpoint, new NewtonsoftJsonSerializer());
    client.HttpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {TOKEN}");

    var consignments = await QueryConsignments(client);
    Console.WriteLine($"We fetched: {consignments.Count()} consignments");

    var number = consignments.First().Number;
    var queriedConsignment = await QueryConsignment(client, number);
    Console.WriteLine($"We queried and found a match input: {number}, result: {queriedConsignment.Number}");

    var createdConsignment = await CreateConsignment(client);
    Console.WriteLine($"We created a consignment with number: {createdConsignment.Number}");

    var updatedConsignment = await UpdateConsignment(client, createdConsignment.Number);
    Console.WriteLine($"We updated a consignment with number: {createdConsignment.Number} from {createdConsignment.Answers.First(x => x.QuestionId == "4" && x.Index == 0).Value} to {updatedConsignment.Answers.First(x => x.QuestionId == "4" && x.Index == 0).Value}");

    var hasDeletedConsignment = await DeleteConsignment(client, createdConsignment.Number);
    Console.WriteLine($"We deleted a consignment with number: {createdConsignment.Number}");
}

await RunAll();