Last updated Sep 25, 2023

Rate this page:

PAPI Client (NodeJS)

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

Dependencies

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"
  }
}

Schemas

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
2
type Query {
    partner: Partner
}

partner.graphqls

1
2
directive @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
2
enum PartnerCloudLicenseType {
    FREE
    COMMERCIAL
    ACADEMIC
    COMMUNITY
    OPEN_SOURCE
    EVALUATION
    STARTER
    DEVELOPER
    DEMONSTRATION
}

enum PartnerBtfLicenseType {
    COMMERCIAL
    ACADEMIC
    EVALUATION
    STARTER
}

enum PartnerCurrency {
    USD
    JPY
}

Queries

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
2
query FetchOfferingCatalog {
    partner {
        offeringCatalog {
            btfProducts {
                productDescription
                productKey
            }
            cloudProducts {
                id
                name
            }
        }
    }
}

partnerOfferingDetailsQuery.graphql

1
2
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
                }
            }
        }
    }
}

Interfaces

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
2
export 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
2
export 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
2
export 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
}

Client

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
2
PORT=3000
CONTAINER_TOKEN="<container token here>"

papiClient.ts

1
2
import 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;

Service

Now we can create a service called papiService inside of src/ that uses papiClient to call offeringCatalog and offeringDetails from AGG:

1
2
import { 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
    }
}

Offering Catalog

We can now import catalogService to call the offeringCatalog query from AGG

1
2
import * as catalogService from './catalogService';

const fetchProducts = async () => {
    try {
        const response = await (catalogService.fetchPartnerOfferingCatalog());
    } catch (error) {
        console.error(error); // Log the error
    }
}

Offering Details + Apps

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

Cloud products can be fetched by passing a cloud product filter

1
2
import * 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

BTF products can be fetched by passing a BTF product filter

1
2
import * 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: