Rate this page:
The following is a simplified implementation of a PAPI client in NodeJS intended to be used as a reference
Node version at the time of implementation: Node 16
The folder structure we have defined is as follows (some files omitted for readability):
1 2├── src │ ├── graphql │ │ ├── queries │ │ ├── schemas │ │── interfaces │ │ │── offering ├── .env ├── package.json
Here is the package.json
which includes dependencies and configuration for Typescript
1 2{ "name": "papi-client-example-node", "version": "1.0.0", "description": "This README would normally document whatever steps are necessary to get your application up and running.", "main": "dist/index.js", "scripts": { "build": "tsc", "prestart": "npm run build", "start": "node .", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@types/express": "^4.17.17", "@types/node": "^16.18.37", "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", "typescript": "^4.9.5" }, "dependencies": { "axios": "^1.4.0", "dotenv": "^16.3.1", "express": "^4.18.2" } }
Here are the schemas that are needed to make GraphQL calls to Atlassian GraphQL Gateway (AGG). These files go
in src/graphql/schemas
:
queries.graphqls
1 2type Query { partner: Partner }
partner.graphqls
1 2directive @oneOf on INPUT_OBJECT type Partner { "Get all cloud and BTF product offerings and pricing information" offeringCatalog: PartnerOfferingListResponse "Get offering and pricing details for a given product, including related apps" offeringDetails( where: PartnerOfferingFilter ): PartnerOfferingDetailsResponse } "Search for available product offerings" input PartnerOfferingFilter @oneOf { "Search cloud offerings by product key" cloudProduct: PartnerOfferingCloudInput "Search BTF offerings by product key" btfProduct: PartnerOfferingBtfInput } input PartnerOfferingCloudInput { "Unique identifier for a cloud product" id: ID! "Available currencies for a cloud product or app" currency: [PartnerCurrency] "Available license types for a cloud offering" pricingPlanType: [PartnerCloudLicenseType] } input PartnerOfferingBtfInput { "Unique identifier for a BTF product" productKey: ID! "Available currencies for a BTF product or app" currency: [PartnerCurrency] "Available license types for a BTF offering" licenseType: [PartnerBtfLicenseType] } interface PartnerCloudProductNode { id: ID! name: String } interface PartnerOfferingNode { id: ID! name: String } interface PartnerPricingPlanNode { id: ID! description: String currency: String type: String } interface PartnerBtfProductNode { productKey: ID! productDescription: String } interface PartnerOrderableItemNode { orderableItemId: ID! description: String licenseType: String currency: String } type PartnerOfferingDetailsResponse { cloudProducts: [PartnerCloudProduct] cloudApps: [PartnerCloudApp] btfProducts: [PartnerBtfProduct] btfApps: [PartnerBtfProduct] } type PartnerOfferingListResponse { cloudProducts: [PartnerCloudProductItem] btfProducts: [PartnerBtfProductItem] } type PartnerCloudProductItem implements PartnerCloudProductNode { id: ID! name: String } type PartnerCloudProduct implements PartnerCloudProductNode { id: ID! name: String chargeElements: [String] uncollectibleAction: PartnerUncollectibleAction offerings: [PartnerOfferingItem] } type PartnerOfferingItem implements PartnerOfferingNode { id: ID! name: String sku: String level: Int supportedBillingSystems: [String] hostingType: String pricingType: String billingType: String parent: String pricingPlans: [PartnerPricingPlan] } type PartnerCloudApp implements PartnerOfferingNode { id: ID! name: String sku: String level: Int supportedBillingSystems: [String] hostingType: String pricingType: String billingType: String parent: String } type PartnerPricingPlan implements PartnerPricingPlanNode { id: ID! description: String currency: String type: String sku: String primaryCycle: PartnerBillingCycle items: [PartnerPricingPlanItem] } type PartnerPricingPlanItem { tiersMode: String chargeElement: String chargeType: String cycle: PartnerBillingCycle tiers: [PartnerPricingTier] } type PartnerPricingTier { ceiling: Float amount: Float flatAmount: Float unitAmount: Float floor: Float policy: String } type PartnerUncollectibleAction { type: String destination: PartnerUncollectibleDestination } type PartnerUncollectibleDestination { offeringKey: ID! } type PartnerBillingCycle { name: String count: Int interval: String } type PartnerBtfProductItem implements PartnerBtfProductNode { productKey: ID! productDescription: String } type PartnerBtfProduct implements PartnerBtfProductNode { productKey: ID! productDescription: String productType: String discountOptOut: Boolean lastModified: String marketplaceAddon: Boolean contactSalesForAdditionalPricing: Boolean dataCenter: Boolean userCountEnforced: Boolean parentDescription: String parentKey: String billingType: String orderableItems: [PartnerOrderableItem] monthly: [PartnerBillingSpecificTier] annual: [PartnerBillingSpecificTier] } type PartnerOrderableItem implements PartnerOrderableItemNode { orderableItemId: ID! newPricingPlanItem: String description: String publiclyAvailable: Boolean saleType: String amount: Float renewalAmount: Float licenseType: String unitCount: Int monthsValid: Int editionDescription: String editionId: String editionType: String editionTypeIsDeprecated: Boolean unitLabel: String enterprise: Boolean starter: Boolean sku: String edition: String renewalFrequency: String currency: String } type PartnerBillingSpecificTier { unitStart: Int unitLimit: Int unitBlockSize: Int price: Float editionType: String entitionTypeIsDepercated: Boolean unitLabel: String currency: String }
common.graphqls
1 2enum PartnerCloudLicenseType { FREE COMMERCIAL ACADEMIC COMMUNITY OPEN_SOURCE EVALUATION STARTER DEVELOPER DEMONSTRATION } enum PartnerBtfLicenseType { COMMERCIAL ACADEMIC EVALUATION STARTER } enum PartnerCurrency { USD JPY }
Here are the queries that we will use to make GraphQL calls to Atlassian GraphQL Gateway (AGG). These files go in
src/graphql/queries
:
partnerOfferingCatalogQuery.graphql
1 2query FetchOfferingCatalog { partner { offeringCatalog { btfProducts { productDescription productKey } cloudProducts { id name } } } }
partnerOfferingDetailsQuery.graphql
1 2query 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 } } } } }
Here are some interfaces we will be needing for sending and receiving GraphQL requests to and from AGG. This file will go inside of src/main/interfaces
interfaces.ts
1 2export interface GraphQlRequestBody { query: string, variables: object, operationName?: string } export interface PapiResponse { data: any }
Here are the interfaces that translate the .graphqls
files into Java classes. These files go inside of src/interfaces/offering
:
requestInterfaces.ts
1 2export interface PartnerOfferingFilter { cloudProduct?: PartnerOfferingCloudInput; btfProduct?: PartnerOfferingBtfInput; } export interface PartnerOfferingCloudInput { id: string; offeringKey?: Array<string>; pricingPlanKey?: Array<string>; currency?: Array<Currency>; pricingPlanType?:Array<CloudLicenseType> } export interface PartnerOfferingBtfInput { productKey: string; currency?: Array<Currency>; orderableItemId?: Array<string>; licenseType?: Array<BtfLicenseType>; } enum Currency { USD, JPY, } enum CloudLicenseType { FREE="Free", COMMERCIAL="Commercial", ACADEMIC="Academic", COMMUNITY="Community", OPEN_SOURCE="Open Source", EVALUATION="Evaluation", STARTER="Starter", DEVELOPER="Developer", FOUNDATION_FREE="Foundation Free", DEMONSTRATION="Demonstration", CLASSROOM="Classroom" } enum BtfLicenseType { COMMERCIAL = "Commercial", ACADEMIC = "Academic", EVALUATION = "Evaluation", STARTER = "Starter", }
responseInterfaces.ts
1 2export interface Partner { offeringCatalog: PartnerOfferingListResponse offeringDetails: PartnerOfferingDetailsResponse } export interface PartnerOfferingListResponse { cloudProducts: [PartnerCloudProductItem] btfProducts: [PartnerBtfProductItem] } export interface PartnerOfferingDetailsResponse { cloudProducts: [PartnerCloudProduct] cloudApps: [PartnerCloudApp] btfProducts: [PartnerBtfProduct] btfApps: [PartnerBtfProduct] } interface PartnerCloudProductNode { id: string name: string } interface PartnerOfferingNode { id: string name: string } interface PartnerPricingPlanNode { id: string description: string currency: string type: string } interface PartnerBtfProductNode { productKey: string productDescription: string } interface PartnerOrderableItemNode { orderableItemId: string description: string licenseType: string currency: string } export interface PartnerCloudProductItem extends PartnerCloudProductNode { id: string name: string } export interface PartnerCloudProduct extends PartnerCloudProductNode { id: string name: string chargeElements: [string] uncollectibleAction: PartnerUncollectibleAction offerings: [PartnerOfferingItem] } export interface PartnerOfferingItem extends PartnerOfferingNode { id: string name: string sku: string level: number supportedBillingSystems: [string] hostingType: string pricingType: string billingType: string parent: string pricingPlans: [PartnerPricingPlan] } export interface PartnerCloudApp extends PartnerOfferingNode { id: string name: string sku: string level: number supportedBillingSystems: [string] hostingType: string pricingType: string billingType: string parent: string } export interface PartnerPricingPlan extends PartnerPricingPlanNode { id: string description: string currency: string type: string sku: string primaryCycle: PartnerBillingCycle items: [PartnerPricingPlanItem] } export interface PartnerPricingPlanItem { tiersMode: string chargeElement: string chargeType: string cycle: PartnerBillingCycle tiers: [PartnerPricingTier] } export interface PartnerPricingTier { ceiling: number amount: number flatAmount: number unitAmount: number floor: number policy: string } export interface PartnerUncollectibleAction { type: string destination: PartnerUncollectibleDestination } export interface PartnerUncollectibleDestination { offeringKey: string } export interface PartnerBillingCycle { name: string count: number interval: string } export interface PartnerBtfProductItem extends PartnerBtfProductNode { productKey: string productDescription: string } export interface PartnerBtfProduct extends PartnerBtfProductNode { productKey: string productDescription: string productType: string discountOptOut: Boolean lastModified: string marketplaceAddon: Boolean contactSalesForAdditionalPricing: Boolean dataCenter: Boolean userCountEnforced: Boolean parentDescription: string parentKey: string billingType: string orderableItems: [PartnerOrderableItem] monthly: [PartnerBillingSpecificTier] annual: [PartnerBillingSpecificTier] } export interface PartnerOrderableItem extends PartnerOrderableItemNode { orderableItemId: string newPricingPlanItem: string description: string publiclyAvailable: Boolean saleType: string amount: number renewalAmount: number licenseType: string unitCount: number monthsValid: number editionDescription: string editionId: string editionType: string editionTypeIsDeprecated: Boolean unitLabel: string enterprise: Boolean starter: Boolean sku: string edition: string renewalFrequency: string currency: string } export interface PartnerBillingSpecificTier { unitStart: number unitLimit: number unitBlockSize: number price: number editionType: string entitionTypeIsDepercated: Boolean unitLabel: string currency: string }
First, we need to set a container token value inside of .env
.
Then, we will create a class called papiClient
inside of src/
which can be used to call AGG
.env
1 2PORT=3000 CONTAINER_TOKEN="<container token here>"
papiClient.ts
1 2import axios, { AxiosInstance } from 'axios'; import dotenv from 'dotenv'; dotenv.config(); // Load variables from .env file const client: AxiosInstance = axios.create({ baseURL: 'https://start.stg.atlassian.com/gateway/api/graphql', // Set your base URL here headers: { "content-type": "application/json", "authorization": 'Bearer ' + process.env.CONTAINER_TOKEN } }); export default client;
Now we can create a service called papiService
inside of src/
that uses papiClient
to call offeringCatalog
and offeringDetails
from AGG:
1 2import { AxiosResponse } from 'axios'; import { readFileSync } from 'fs'; import { GraphQlRequestBody } from './interfaces/interfaces'; import { PartnerOfferingFilter } from './interfaces/offering/requestInterfaces'; import { Partner } from './interfaces/offering/responseInterfaces'; import client from './papiClient'; export const fetchPartnerOfferingCatalog = async (): Promise<AxiosResponse<Partner, GraphQlRequestBody> | undefined> => { // get schema from graphql file const filePath = './src/graphql/queries/partnerOfferingCatalogQuery.graphql'; const query = readFileSync(filePath, 'utf-8'); const graphqlQuery: GraphQlRequestBody = { operationName: 'FetchOfferingCatalog', query, variables: {} }; console.log(graphqlQuery); try { const response = await client.post( '', graphqlQuery, ) console.log(response.data); // data return response; } catch (error) { console.log(error); // errors if any } } export const fetchPartnerOfferingDetails = async (partnerOfferingFilter: PartnerOfferingFilter): Promise<AxiosResponse<Partner, GraphQlRequestBody> | undefined> => { // get schema from graphql file const filePath = './src/graphql/queries/partnerOfferingDetailsQuery.graphql'; const query = readFileSync(filePath, 'utf-8'); const graphqlQuery: GraphQlRequestBody = { operationName: 'FetchOfferingDetails', query, variables: { where: partnerOfferingFilter } }; console.log(graphqlQuery); try { const response = await client.post( '', graphqlQuery, ) console.log(response.data); // data return response; } catch (error) { console.log(error); // errors if any } }
We can now import catalogService
to call the offeringCatalog
query from AGG
1 2import * as catalogService from './catalogService'; const fetchProducts = async () => { try { const response = await (catalogService.fetchPartnerOfferingCatalog()); } catch (error) { console.error(error); // Log the error } }
We can now import catalogService
to call the offeringDetails
query from AGG
The offeringDetails
query has a whereFilter which is required to specify either a cloud or a BTF product identifier,
along with additional optional parameters. This is defined as the PartnerOfferingFilter
GraphQL type.
We can either fetch cloud or BTF product and app details. Only one product can be fetched at a time.
Cloud products can be fetched by passing a cloud product filter
1 2import * as catalogService from './catalogService'; const testPartnerOfferingDetails = async () => { try { const partnerOfferingFilter: PartnerOfferingFilter = { cloudProduct: { id: "0b8ea1e2-52df-11ea-8d77-2e728ce88125" } } const response = await (catalogService.fetchPartnerOfferingDetails(partnerOfferingFilter)); res.json(response?.data) } catch (error) { console.error(error); // Log the error } }
BTF products can be fetched by passing a BTF product filter
1 2import * as catalogService from './catalogService'; const fetchBtfProduct = async () => { try { const partnerOfferingFilter: PartnerOfferingFilter = { btfProduct: { productKey: "confluence" } } const response = await (catalogService.fetchPartnerOfferingDetails(partnerOfferingFilter)); res.json(response?.data) } catch (error) { console.error(error); // Log the error } }
Rate this page: