Last updated Sep 25, 2023

Rate this page:

PAPI Client (Go)

The following is a simplified implementation of a PAPI client in Go Lang intended to be used as a reference

Go version at the time of implementation: go 1.21rc1

The folder structure we have defined is as follows:

1
2
├── client
│ ├── papi
│ │ ├── papi.go
├── service
│ ├── catalog
│ │ ├── catalog.go
│── go.mod
│ ├── go.sum
└── main.go

Dependencies

We are leveraging the following library for the GraphQL requests: machinebox/graphql

1
2
require github.com/machinebox/graphql v0.2.2

Client

The PAPI client is first initialized in papi package by the init() function:

1
2
// /client/papi/papi.go

package papi

import (
	"github.com/machinebox/graphql"
	"os"
)

var Client *graphql.Client

func init() {

	// Initialize the PAPI client
	Client = graphql.NewClient("https://api.atlassian.com/graphql")
}

Now, let's add a couple supporting methods that we'll use to set the authorization header as well as build a GraphQL query request. With this, we are able to dynamically pass a query to the client from each service.

For simplicity, TOKEN is passed in as a runtime property

1
2
func buildAuthHeader() string {
	return "Bearer " + os.Getenv("TOKEN")
}

func Query(query string) *graphql.Request {

	// Create new request 
	req := graphql.NewRequest(query)

	req.Header.Add("Authorization", buildAuthHeader())

	return req
}

Services

Each service can now import the PAPI client to facilitate requests

Catalog

1
2
// /service/catalog/catalog.go

package catalog

import (
	"context"
	"log"
	"papi-client-go-example/client/papi"
)

Offering Catalog

Supporting types for the OfferingCatalog query response:

1
2
type OfferingCatalogResponse struct {
	Partner struct {
		OfferingCatalog struct {
			BtfProducts []struct {
				ProductKey         string `json:"productKey"`
				ProductDescription string `json:"productDescription"`
			} `json:"btfProducts"`
			CloudProducts []struct {
				ID   string `json:"id"`
				Name string `json:"name"`
			} `json:"cloudProducts"`
		} `json:"offeringCatalog"`
	} `json:"partner"`
}

Let's create a function which we can use to make a OfferingCatalog request:

1
2
func FetchOfferingCatalog() OfferingCatalogResponse {
	req := papi.Query(`
		query fetchOfferingCatalog{
			partner {
				offeringCatalog{
					btfProducts{
						productKey
						productDescription
					}
					cloudProducts{
						id
						name
					}
				}
			}
		}
	`)

	var response OfferingCatalogResponse
	if err := papi.Client.Run(context.Background(), req, &response); err != nil {
		log.Fatal(err)
	}

	return response
}

Now that we've created the catalog service request for OfferingCatalog query, we can import it and call the service

1
2
package main

import (
	"encoding/json"
	"log"
	papiCatalogService "papi-client-go-example/service/catalog"
)

func main() {
	/**
	   Fetch OfferingCatalog
	*/

	// Fetch the offering catalog
	response := papiCatalogService.FetchOfferingCatalog()

	// Convert the response to JSON
	res, err := json.Marshal(response)

	if err != nil {
		panic(err)
	}

	log.Println("FetchOfferingCatalog | response: ", string(res))
}

Offering Details + Apps

Supporting type structs for the OfferingDetails query response:

1
2
type OfferingDetailsResponse struct {
	Partner struct {
		OfferingDetails struct {
			CloudProducts []struct {
				ID        string `json:"id"`
				Name      string `json:"name"`
				Offerings []struct {
					BillingType  interface{} `json:"billingType"`
					HostingType  string      `json:"hostingType"`
					ID           string      `json:"id"`
					Level        int         `json:"level"`
					Name         string      `json:"name"`
					PricingType  string      `json:"pricingType"`
					Sku          string      `json:"sku"`
					PricingPlans []struct {
						Currency    string `json:"currency"`
						Description string `json:"description"`
						Items       []struct {
							ChargeElement string `json:"chargeElement"`
							ChargeType    string `json:"chargeType"`
							Cycle         struct {
								Count    int    `json:"count"`
								Interval string `json:"interval"`
								Name     string `json:"name"`
							} `json:"cycle"`
							Tiers []struct {
								Amount     interface{} `json:"amount"`
								Ceiling    float64     `json:"ceiling"`
								FlatAmount interface{} `json:"flatAmount"`
								Floor      float64     `json:"floor"`
								Policy     interface{} `json:"policy"`
								UnitAmount float64     `json:"unitAmount"`
							} `json:"tiers"`
							TiersMode string `json:"tiersMode"`
						} `json:"items"`
						ID           string `json:"id"`
						PrimaryCycle struct {
							Count    int    `json:"count"`
							Interval string `json:"interval"`
							Name     string `json:"name"`
						} `json:"primaryCycle"`
						Sku  string `json:"sku"`
						Type string `json:"type"`
					} `json:"pricingPlans"`
				} `json:"offerings"`
			} `json:"cloudProducts,omitempty"`
			CloudApps []struct {
				BillingType             interface{} `json:"billingType"`
				HostingType             interface{} `json:"hostingType"`
				ID                      string      `json:"id"`
				Level                   int         `json:"level"`
				Name                    string      `json:"name"`
				Parent                  interface{} `json:"parent"`
				PricingType             interface{} `json:"pricingType"`
				Sku                     string      `json:"sku"`
				SupportedBillingSystems interface{} `json:"supportedBillingSystems"`
			} `json:"cloudApps,omitempty"`
			BtfProducts []struct {
				BillingType                      string        `json:"billingType"`
				ContactSalesForAdditionalPricing bool          `json:"contactSalesForAdditionalPricing"`
				DataCenter                       bool          `json:"dataCenter"`
				DiscountOptOut                   bool          `json:"discountOptOut"`
				LastModified                     string        `json:"lastModified"`
				MarketplaceAddon                 bool          `json:"marketplaceAddon"`
				ParentKey                        interface{}   `json:"parentKey"`
				ParentDescription                interface{}   `json:"parentDescription"`
				ProductDescription               string        `json:"productDescription"`
				ProductKey                       string        `json:"productKey"`
				UserCountEnforced                bool          `json:"userCountEnforced"`
				ProductType                      string        `json:"productType"`
				Monthly                          []interface{} `json:"monthly"`
				OrderableItems                   []struct {
					Amount                  int         `json:"amount"`
					Currency                string      `json:"currency"`
					Description             string      `json:"description"`
					Edition                 interface{} `json:"edition"`
					EditionDescription      string      `json:"editionDescription"`
					EditionID               string      `json:"editionId"`
					EditionType             string      `json:"editionType"`
					EditionTypeIsDeprecated bool        `json:"editionTypeIsDeprecated"`
					Enterprise              bool        `json:"enterprise"`
					LicenseType             string      `json:"licenseType"`
					MonthsValid             int         `json:"monthsValid"`
					NewPricingPlanItem      interface{} `json:"newPricingPlanItem"`
					OrderableItemID         string      `json:"orderableItemId"`
					PubliclyAvailable       bool        `json:"publiclyAvailable"`
					RenewalAmount           int         `json:"renewalAmount"`
					RenewalFrequency        string      `json:"renewalFrequency"`
					SaleType                string      `json:"saleType"`
					Sku                     interface{} `json:"sku"`
					Starter                 bool        `json:"starter"`
					UnitCount               int         `json:"unitCount"`
					UnitLabel               string      `json:"unitLabel"`
				} `json:"orderableItems"`
			} `json:"btfProducts,omitempty"`
			BtfApps []struct {
				BillingType                      string `json:"billingType"`
				ContactSalesForAdditionalPricing bool   `json:"contactSalesForAdditionalPricing"`
				DataCenter                       bool   `json:"dataCenter"`
				DiscountOptOut                   bool   `json:"discountOptOut"`
				LastModified                     string `json:"lastModified"`
				MarketplaceAddon                 bool   `json:"marketplaceAddon"`
				ParentDescription                string `json:"parentDescription"`
				ParentKey                        string `json:"parentKey"`
				ProductDescription               string `json:"productDescription"`
				ProductKey                       string `json:"productKey"`
				ProductType                      string `json:"productType"`
				UserCountEnforced                bool   `json:"userCountEnforced"`
				OrderableItems                   []struct {
					Amount                  int         `json:"amount"`
					Currency                string      `json:"currency"`
					Description             string      `json:"description"`
					Edition                 interface{} `json:"edition"`
					EditionDescription      string      `json:"editionDescription"`
					EditionID               string      `json:"editionId"`
					EditionType             string      `json:"editionType"`
					EditionTypeIsDeprecated bool        `json:"editionTypeIsDeprecated"`
					Enterprise              bool        `json:"enterprise"`
					MonthsValid             int         `json:"monthsValid"`
					LicenseType             string      `json:"licenseType"`
					NewPricingPlanItem      interface{} `json:"newPricingPlanItem"`
					OrderableItemID         string      `json:"orderableItemId"`
					PubliclyAvailable       bool        `json:"publiclyAvailable"`
					RenewalAmount           int         `json:"renewalAmount"`
					SaleType                string      `json:"saleType"`
					RenewalFrequency        string      `json:"renewalFrequency"`
					Sku                     interface{} `json:"sku"`
					Starter                 bool        `json:"starter"`
					UnitCount               int         `json:"unitCount"`
					UnitLabel               string      `json:"unitLabel"`
				} `json:"orderableItems"`
				Monthly []interface{} `json:"monthly"`
				Annual  []interface{} `json:"annual"`
			} `json:"btfApps,omitempty"`
		} `json:"offeringDetails"`
	} `json:"partner"`
}

For the details query we have a filter which is required to specify either a cloud of a BTF product identifier, along with additional optional parameters. This is defined as the PartnerOfferingFilter GraphQL type. We will create the supporting type definitions in Go to implement this.

1
2
type OfferingFilter struct {
	CloudProductFilter *CloudFilter `json:"cloudProduct,omitempty"`
	BtfProductFilter   *BtfFilter   `json:"btfProduct,omitempty"`
}

type CloudFilter struct {
	Id              string `json:"id,omitempty"`
	Currency        string `json:"currency,omitempty"`
	PricingPlanType string `json:"pricingPlanType,omitempty"`
}

type BtfFilter struct {
	ProductKey  string `json:"productKey,omitempty"`
	Currency    string `json:"currency,omitempty"`
	LicenseType string `json:"licenseType,omitempty"`
}

Let's create a function which we can use to make a OfferingDetails request:

1
2
func FetchOfferingDetails(filter OfferingFilter) OfferingDetailsResponse {
 req := papi.Query(`
		query fetchOfferingDetails($where: PartnerOfferingFilter) {
		  partner {
			offeringDetails(where: $where){
			  cloudProducts {
				id
				name
				offerings {
				  billingType
				  hostingType
				  id
				  level
				  name
				  pricingType
				  sku
				  pricingPlans {
					currency
					description
					items {
					  chargeElement
					  chargeType
					  cycle {
						count
						interval
						name
					  }
					  tiers {
						amount
						ceiling
						flatAmount
						floor
						policy
						unitAmount
					  }
					  tiersMode
					}
					id
					primaryCycle{
					  count
					  interval
					  name
					}
					sku
					type
				  }
				}
			  }
			  cloudApps {
				billingType
				hostingType
				id
				level
				name
				parent
				pricingType
				sku
				supportedBillingSystems
			  }
			  btfProducts {
				billingType
				contactSalesForAdditionalPricing
				dataCenter
				discountOptOut
				lastModified
				marketplaceAddon
				parentKey
				parentDescription
				productDescription
				productKey
				userCountEnforced
				productType
				monthly {
				  currency
				  editionType
				  entitionTypeIsDepercated
				  price
				  unitBlockSize
				  unitLabel
				  unitLimit
				  unitStart
				}
				orderableItems {
				  amount
				  currency
				  description
				  edition
				  editionDescription
				  editionId
				  editionType
				  editionTypeIsDeprecated
				  enterprise
				  licenseType
				  monthsValid
				  newPricingPlanItem
				  orderableItemId
				  publiclyAvailable
				  renewalAmount
				  renewalFrequency
				  saleType
				  sku
				  starter
				  unitCount
				  unitLabel
				}
			  }
			  btfApps {
				billingType
				contactSalesForAdditionalPricing
				dataCenter
				discountOptOut
				lastModified
				marketplaceAddon
				parentDescription
				parentKey
				productDescription
				productKey
				productType
				userCountEnforced
				orderableItems {
				  amount
				  currency
				  description
				  edition
				  editionDescription
				  editionId
				  editionType
				  editionTypeIsDeprecated
				  enterprise
				  monthsValid
				  licenseType
				  newPricingPlanItem
				  orderableItemId
				  publiclyAvailable
				  renewalAmount
				  saleType
				  renewalFrequency
				  sku
				  starter
				  unitCount
				  unitLabel
				}
				monthly {
				  currency
				  editionType
				  entitionTypeIsDepercated
				  price
				  unitBlockSize
				  unitLabel
				  unitLimit
				  unitStart
				}
				annual {
				  currency
				  editionType
				  entitionTypeIsDepercated
				  price
				  unitBlockSize
				  unitLabel
				  unitLimit
				  unitStart
				}
			  }
			}
		  }
		}
	`)

	req.Var("where", filter)

	var response OfferingDetailsResponse
	if err := papi.Client.Run(context.Background(), req, &response); err != nil {
		log.Fatal(err)
	}

	return response
}

Now that we've created the catalog service request for OfferingDetails query, we can either fetch cloud or BTF product and app details. Only one product can be fetched at a time.

Cloud

Cloud products can be fetched by passing a cloud product filter to the FetchOfferingDetails service

1
2
package main

import (
	"encoding/json"
	"log"
	papiCatalogService "papi-client-go-example/service/catalog"
)

func main() {
	/**
	   Fetch OfferingDetails (Cloud)
	*/
	
	// Construct the filter
	filter := papiCatalogService.OfferingFilter{
		CloudProductFilter: &papiCatalogService.CloudFilter{
			Id: "6aab3f4e-cc71-474d-8d28-abef8b057808",
		},
	}
	
	// Fetch the offering details
	response := papiCatalogService.FetchOfferingDetails(filter)

	// Convert the response to JSON
	res, err := json.Marshal(response)

	if err != nil {
		panic(err)
	}

	log.Println("FetchOfferingDetails (Cloud) | response: ", string(res))
}
BTF

BTF products can be fetched by passing a BTF product filter to the FetchOfferingDetails service

1
2
package main

import (
	"encoding/json"
	"log"
	papiCatalogService "papi-client-go-example/service/catalog"
)

func main() {
	/**
	   Fetch OfferingDetails (BTF)
	*/
	
	// Construct the filter
	filter := papiCatalogService.OfferingFilter{
		BtfProductFilter: &papiCatalogService.BtfFilter{
			ProductKey: "confluence",
		},
	}
	
	// Fetch the offering details
	response := papiCatalogService.FetchOfferingDetails(filter)

	// Convert the response to JSON
	res, err := json.Marshal(response)

	if err != nil {
		panic(err)
	}

	log.Println("FetchOfferingDetails (BTF) | response: ", string(res))
}

Rate this page: