eNVD API for developers

About

eNVD

eNVD stands for electronic National Vendor Declaration. The Livestock Production Assurance NVD communicates the food safety and treatment status of every animal every time it moves – between properties, to saleyards, or to processors. The NVD also acts as movement documentation throughout the value chain.

NVDs are a legal document that are key to Australian red meat’s traceability and market access. It is crucial that the NVD is filled out accurately, when users tick a box or answer a question on the LPA NVD, they are declaring their on-farm practices meet LPA requirements, and ultimately customer expectations.

Their tick must be backed up by accurate farm records. This is a pledge that the meat from their farm has been produced safely, ethically and meets biosecurity requirements– it means they stand by what you sell.

eNVD is the digital process for assuring and moving livestock. It is a fast and easy way for users to complete livestock consignments and maintain LPA records. The system will be continuously improved to create better experiences for users and add value to our industry’s integrity. For more information, visit the ISC website here.

The eNVD API is available to integrators that deliver livestock assurance and movement related solutions to the Australian red meat industry. Licensed integrators are transitioning from REST to GraphQL with further details below.

About GraphQL

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.

GraphQL has three main characteristics:

  • It lets the client specify exactly what data it needs.

  • It makes it easier to aggregate data from multiple sources.

  • It uses a type system to describe data.

With GraphQL, the client is able to make a single call to fetch the required information rather than to construct several REST requests to fetch the same. The client is also able to easily understand what is available in the API and how to request it as the endpoint itself provides such documentation via an introspection query.

eNVD uses GraphQL as it offers more flexibility for developers. The option to precisely create or retrieve a consignment (or multiple consignments) including the information that a user wants is a great advantage over sending multiple REST calls to achieve the same.

Visit the GraphQL website to learn more about how GraphQL works.

Authentication

eNVD requires clients to use an API key which will allow application users to login.
You can then login users from 2 different systems.

  • LPA users - Full read/write access

  • NLIS users - Read access, read/write access for transporters

You can apply for a API key by emailing technicalsupport@IntegritySystems.com.au.

The eNVD API uses JSON web tokens. When an HTTP POST request is sent to the Auth Server, a valid token is returned or an invalid request. The returned token must then be passed in as a request header with all future requests.

Request headers

HTTP Header

Description

Example

HTTP Header

Description

Example

Content-type

The MIME type of the body of the request

Content-Type: application/x-www-form-urlencoded

Request body

HTTP Body

Description

Example

HTTP Body

Description

Example

Client ID

Client identifier of your application

client_id=mySuperApp

Client Secret

Your assigned client secret

client_secret=mysecret

Grant Type

We use Password Grant Type

grant_type=password

Scope

Available values include lpa_scope or nlis_scope, dependent on your user

scope=lpa_scope

UserName

When the scope is for LPA users, the username is the PIC-userid (note the - between PIC and userid)
When the scope is for NLIS users, the username is your NLIS userID.

username=Q1KK0786-1005672
username=9PROD2G0

Password

The account holder's relevant password

password=userpassword

Example header

1 Content-Type: application/x-www-form-urlencoded

Example body

1 client_id=mySuperApp&client_secret=mysecret&grant_type=password&scope=lpa_scope&username=Q1KK0786-1005672&password=userpassword

Response body

HTTP Body

Description

Example

HTTP Body

Description

Example

access_token

The access token returned

"access_token" : "eyJ0eXAiOiJKV1Q..."

expires_in

Number of minutes the token will be valid for

"expires_in" : "3600"

token_type

The type of token generated by the API

"Bearer"

Example body

1 2 3 4 5 { "access_token" : "eyJ0eXAiOiJKV1Q...", "expires_in" : "3600", "token_type" : "Bearer" }

Response codes

If a response code of 200 is returned, it means you have successfully authenticated and can access your token.

If a response code of 400 is returned, it could mean that your api key is invalid by indicating you are an invalid client or your username or password is invalid.

For other response codes, check the message included in the body of the response object.

Program accreditation

To be able to create an NVD via the eNVD system, a user must be LPA accredited. The table below outlines the program accreditation required for each type of form supported by eNVD.

 

 

 

 

 

 

Forms

 

Program Accreditation

 

LPA

EU

MSA

NFAS

NVD

 

 

 

NVD EU

 

 

MSA

 

 

NFAS

 

 

NFAS EU

 

Health Dec

 

 

 

 

Using the GraphQL API

Must like a REST API, interacting with a GraphQL API involving making calls to the endpoint in way that follows GraphQL rules and the types of the API. For initial learning and experimentation we recommend two use either:

  1. A locally installed IDE like Postman and Insomnia

  2. Playground, a web based interface which is available by navigating in your browser to one of our GraphQL API endpoints

Please review the documentation for each tool to learn how to use it to interact with a GraphQL API.

Examples

The following examples provide some eNVD specific context to how interactions with the GraphQL API work. Please bare in mind that these are examples only and queries should be modified to suit your needs, such as reducing the fields in the requests to only ask for the data you need.

Retrieving a consignment

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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 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 } } } }

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 // 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 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; }

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

1 2 3 4 5 6 # Or request a single consignment query { consignment(id: "C-12341234") { ... same as above } }

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 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:

1 2 3 4 5 6 7 8 9 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.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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 } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 // 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.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 // 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.

It contains an array of entries such as:

1 2 3 4 5 6 { "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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "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:

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

1 2 3 4 5 6 7 8 9 { ... "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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 #!/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();