Tutorial 4: Mastering GraphQL Queries and Data Fetching
Objective
Learn how to write efficient GraphQL queries, handle complex data relationships, and optimize data fetching for your forms.
Understanding Open mSupply's GraphQL Schema
Open mSupply exposes data through a GraphQL API. Each context (REQUISITION, STOCKTAKE, etc.) provides access to specific data types and relationships.
Basic Query Structure
query MyFormQuery($storeId: String!, $dataId: String!) {
# Main entity query
requisition(storeId: $storeId, id: $dataId) {
... on RequisitionNode {
# Basic fields
id
status
requisitionNumber
createdDatetime
# Related entities
otherParty(storeId: $storeId) {
name
code
}
}
}
# Store information (usually available in all contexts)
store(id: $storeId) {
... on StoreNode {
storeName
logo
}
}
}Advanced Query Patterns
1. Deep Nested Relationships
query ComplexRequisitionQuery($storeId: String!, $dataId: String!) {
requisition(storeId: $storeId, id: $dataId) {
... on RequisitionNode {
id
status
requisitionNumber
theirReference
createdDatetime
finalisedDatetime
comment
maxMonthsOfStock
# Supplier/Customer information
otherParty(storeId: $storeId) {
name
id
code
address1
address2
phone
email
isSupplier
isCustomer
}
# Line items with detailed information
lines {
nodes {
id
requestedQuantity
supplyQuantity
remainingQuantityToSupply
alreadyIssued
comment
# Item details
item {
id
code
name
unitName
defaultPackSize
}
# Stock statistics
itemStats {
stockOnHand
averageMonthlyConsumption
monthsOfStockOnHand
maximumQuantity
minimumQuantity
}
}
}
}
}
# Extended store information
store(id: $storeId) {
... on StoreNode {
id
code
storeName
logo
# Store contact details
name(storeId: $storeId) {
... on NameNode {
name
address1
address2
code
phone
email
website
country
}
}
}
}
}2. Conditional Queries with Fragments
query RequisitionWithConditionalData(
$storeId: String!
$dataId: String!
$includeStats: Boolean = true
) {
requisition(storeId: $storeId, id: $dataId) {
... on RequisitionNode {
...BasicRequisitionInfo
lines {
nodes {
...LineItemInfo
# Conditional inclusion of stock statistics
itemStats @include(if: $includeStats) {
stockOnHand
averageMonthlyConsumption
monthsOfStockOnHand
}
}
}
}
}
store(id: $storeId) {
...StoreInfo
}
}
fragment BasicRequisitionInfo on RequisitionNode {
id
status
requisitionNumber
createdDatetime
finalisedDatetime
comment
}
fragment LineItemInfo on RequisitionLineNode {
id
requestedQuantity
supplyQuantity
item {
code
name
unitName
}
}
fragment StoreInfo on StoreNode {
id
storeName
logo
name(storeId: $storeId) {
address1
address2
phone
email
}
}3. Handling Different Entity Types
query StocktakeQuery($storeId: String!, $dataId: String!, $sort: PrintReportSortInput) {
stocktake(storeId: $storeId, id: $dataId) {
... on StocktakeNode {
id
stocktakeDate
stocktakeNumber
status
description
comment
}
... on NodeError {
__typename
error {
description
}
}
}
stocktakeLines(storeId: $storeId, stocktakeId: $dataId, reportSort: $sort) {
... on StocktakeLineConnector {
totalCount
nodes {
id
item {
... on ItemNode {
code
name
unitName
}
}
packSize
expiryDate
countedNumberOfPacks
snapshotNumberOfPacks
batch
costPricePerPack
# Location information
location {
id
code
name
onHold
}
# Reason for adjustments
reasonOption {
... on ReasonOptionNode {
id
reason
}
}
}
}
}
store(id: $storeId) {
... on StoreNode {
storeName
logo
}
}
}Query Optimization Techniques
1. Selective Field Querying
Only request fields you actually use in your template:
# Bad - requesting unnecessary fields
query IneffientQuery($storeId: String!, $dataId: String!) {
requisition(storeId: $storeId, id: $dataId) {
... on RequisitionNode {
# Getting all fields even if not used
id
status
requisitionNumber
type
createdDatetime
sentDatetime
finalisedDatetime
expectedDeliveryDatetime
comment
theirReference
maxMonthsOfStock
minMonthsOfStock
approvalStatus
programId
period
orderType
# ... many more fields
}
}
}
# Good - only requesting needed fields
query EfficientQuery($storeId: String!, $dataId: String!) {
requisition(storeId: $storeId, id: $dataId) {
... on RequisitionNode {
id
status
requisitionNumber
createdDatetime
comment
otherParty(storeId: $storeId) {
name
code
}
lines {
nodes {
requestedQuantity
item {
code
name
}
}
}
}
}
}2. Using Sorting and Filtering
query SortedStocktakeQuery(
$storeId: String!
$dataId: String!
$sort: PrintReportSortInput
$filter: StocktakeLineFilterInput
) {
stocktakeLines(
storeId: $storeId
stocktakeId: $dataId
reportSort: $sort
filter: $filter
first: 100 # Limit results for performance
) {
... on StocktakeLineConnector {
totalCount
nodes {
id
item {
code
name
}
packSize
countedNumberOfPacks
snapshotNumberOfPacks
batch
expiryDate
}
}
}
}3. Pagination for Large Datasets
query PaginatedInvoiceLines(
$storeId: String!
$invoiceId: String!
$first: Int = 50
$after: String
$sort: PrintReportSortInput
) {
invoiceLines(
storeId: $storeId
filter: { invoiceId: { equalTo: $invoiceId } }
first: $first
after: $after
reportSort: $sort
) {
... on InvoiceLineConnector {
totalCount
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
nodes {
id
itemName
numberOfPacks
packSize
batch
expiryDate
location {
code
}
}
}
}
}Context-Specific Query Examples
Purchase Order Query
query PurchaseOrderQuery($dataId: String!, $storeId: String!) {
purchaseOrder(storeId: $storeId, id: $dataId) {
... on PurchaseOrderNode {
id
number
status
createdDatetime
expectedDeliveryDatetime
confirmedDatetime
finalisedDatetime
colour
comment
theirReference
# Supplier information
supplier {
id
name
code
address1
address2
phone
email
}
# Purchase order lines
lines {
nodes {
id
lineNumber
itemId
packSize
numberOfPacks
requestedQuantity
approvedQuantity
suppliedQuantity
unitCost
totalCost
item {
id
name
code
unitName
defaultPackSize
}
}
}
# Store making the purchase
store {
id
storeName
logo
name(storeId: $storeId) {
name
address1
address2
phone
email
}
}
# Financial totals
pricing {
subTotal
taxPercentage
taxAmount
totalAfterTax
totalBeforeTax
}
}
}
}Invoice Query with Line Details
query DetailedInvoiceQuery($storeId: String!, $dataId: String!, $sort: PrintReportSortInput) {
invoice(storeId: $storeId, id: $dataId) {
... on InvoiceNode {
id
invoiceNumber
status
type
createdDatetime
allocatedDatetime
pickedDatetime
shippedDatetime
deliveredDatetime
verifiedDatetime
comment
theirReference
# Customer/Supplier information
otherParty(storeId: $storeId) {
id
name
code
address1
address2
phone
email
isCustomer
isSupplier
}
# User who created the invoice
user {
username
displayName
email
}
# Pricing information
pricing {
stockTotalBeforeTax
stockTotalAfterTax
serviceTotalBeforeTax
serviceTotalAfterTax
taxPercentage
totalBeforeTax
totalAfterTax
foreignCurrencyTotalAfterTax
}
# Currency information
currency {
id
code
rate
isHomeCurrency
}
}
}
# Detailed line items
invoiceLines(storeId: $storeId, filter: { invoiceId: { equalTo: $dataId } }, reportSort: $sort) {
... on InvoiceLineConnector {
totalCount
nodes {
id
type
itemCode
itemName
locationName
packSize
numberOfPacks
unitQuantity
batch
expiryDate
sellPricePerPack
costPricePerPack
# Location details
location {
id
code
name
onHold
}
# Item details
item {
id
code
name
unitName
defaultPackSize
isVisible
}
# Stock line information (for tracking)
stockLine {
id
totalQuantity
availableQuantity
onHold
}
# Line pricing
pricing {
totalBeforeTax
totalAfterTax
taxPercentage
}
}
}
}
store(id: $storeId) {
... on StoreNode {
id
code
storeName
logo
name(storeId: $storeId) {
name
address1
address2
code
phone
email
website
}
}
}
}Error Handling in GraphQL Queries
1. Handling Node Errors
query RequisitionWithErrorHandling($storeId: String!, $dataId: String!) {
requisition(storeId: $storeId, id: $dataId) {
... on RequisitionNode {
id
status
requisitionNumber
# ... other fields
}
... on NodeError {
__typename
error {
description
fullError
}
}
... on RecordNotFound {
__typename
description
}
}
}2. Template Error Handling
In your template, handle potential errors:
<!-- In template.html -->
{% if data.requisition.__typename == "NodeError" %}
<div class="error_message">
<h2>Error Loading Requisition</h2>
<p>{{ data.requisition.error.description }}</p>
</div>
{% elif data.requisition.__typename == "RecordNotFound" %}
<div class="error_message">
<h2>Requisition Not Found</h2>
<p>{{ data.requisition.description }}</p>
</div>
{% elif data.requisition %}
<!-- Normal template content -->
<div class="requisition_content">
<!-- Your form content here -->
</div>
{% else %}
<div class="error_message">
<h2>No Data Available</h2>
<p>Unable to load requisition data.</p>
</div>
{% endif %}Query Variables and Arguments
Using Variables Effectively
Update your manifest to accept custom arguments:
{
"is_custom": true,
"version": "1.0.0",
"code": "equipment-request",
"context": "REQUISITION",
"name": "Equipment Request",
"queries": {
"gql": "query.graphql"
},
"arguments": {
"includeStats": true,
"sortBy": "itemName",
"maxItems": 100
},
"header": "header.html"
}
Then use these in your query:
query EquipmentRequestQuery(
$storeId: String!
$dataId: String!
$includeStats: Boolean = true
$sortBy: String = "itemName"
$maxItems: Int = 100
) {
requisition(storeId: $storeId, id: $dataId) {
... on RequisitionNode {
id
status
requisitionNumber
lines(first: $maxItems) {
nodes {
requestedQuantity
item {
code
name
}
itemStats @include(if: $includeStats) {
stockOnHand
averageMonthlyConsumption
}
}
}
}
}
}Performance Optimization Tips
1. Query Complexity Analysis
# High complexity - avoid deep nesting
query ComplexQuery($storeId: String!) {
invoices(storeId: $storeId) {
nodes {
lines {
nodes {
stockLine {
item {
masterlists {
nodes {
lines {
nodes {
# Too many levels!
}
}
}
}
}
}
}
}
}
}
}
# Better - focused query
query EfficientQuery($storeId: String!, $invoiceId: String!) {
invoice(storeId: $storeId, id: $invoiceId) {
... on InvoiceNode {
id
invoiceNumber
lines {
nodes {
itemName
numberOfPacks
item {
code
name
}
}
}
}
}
}2. Field Selection Best Practices
# Instead of requesting all item fields
item {
id
code
name
description
unitName
defaultPackSize
outerPackSize
strength
volume
weight
# ... many more
}
# Only request what you need
item {
code
name
unitName
}Testing Your Queries
1. Using GraphQL Playground
If available, test your queries in GraphQL Playground:
- Navigate to your GraphQL endpoint
- Paste your query
- Add test variables
- Execute and verify results
2. Query Validation
Create a simple test template to validate your query:
<!-- test-template.html -->
<h1>Query Test Results</h1>
<h2>Raw Data Debug</h2>
<pre>{{ data | json_encode(pretty=true) }}</pre>
<h2>Requisition Info</h2>
{% if data.requisition %}
<p>ID: {{ data.requisition.id }}</p>
<p>Number: {{ data.requisition.requisitionNumber }}</p>
<p>Status: {{ data.requisition.status }}</p>
<p>Lines Count: {{ data.requisition.lines.nodes | length }}</p>
{% else %}
<p>No requisition data</p>
{% endif %}
<h2>Store Info</h2>
{% if data.store %}
<p>Name: {{ data.store.storeName }}</p>
<p>Logo: {{ data.store.logo }}</p>
{% else %}
<p>No store data</p>
{% endif %}What We've Learned
- GraphQL query structure and syntax
- Advanced querying techniques (fragments, variables, conditionals)
- Context-specific query patterns
- Error handling in queries and templates
- Performance optimization strategies
- Query testing and validation
In Tutorial 5, we'll cover deployment, versioning, and maintaining your forms in production.