@valiantys/atlassian-app-backend
v1.0.0
Published
This library provides utility functions to handle all the setup necessary to support a Forge app backend that can run deployed or in standalone mode
Downloads
74
Readme
@valiantys/atlassian-app-backend
This library provides utility functions to handle all the setup necessary to support a Forge app backend that can run deployed or in standalone mode.
Note: The library currently only supports resolver functions and Forge Storage in standalone mode (without a tunnel).
Using the library
- Creating a new App
- Run the app locally (standalone mode)
- Deploy the app
- Install
- Configure CORS
- Unit testing
- Example Application
- Support
Creating a new App
Use the Forge CLI to create a new Forge App with a Custom UI
npm install -g @forge/cli@latest
forge create -d forge-app -t jira-global-page-custom-ui
Add Typescript support for the backend
Install the necessary libraries and initialize typescript compiler config.
npm i --save-dev typescript nodemon
npm i dotenv
npx tsc --init
Add "exclude" and "outDir" to tsconfig.json:
{
"exclude": ["node_modules", "dist", "static", "**/*spec.ts"],
"compilerOptions": {
"outDir": "dist",
...
}
}
Add scripts to package.json.
{
"scripts": {
"build": "tsc",
"dev": "tsc -w & nodemon -q -w dist dist/index.js",
"start": "tsc && node ./dist/index.js"
}
}
Install backend library
npm i @valiantys/atlassian-app-backend
Add an environment file
Add a .env file in the root of the project, and add .env to your project's .gitignore file. You should also add a sample.env file to your project that does NOT contain any secrets but makes it easier for other developers to set up their own .env file.
IS_STANDALONE=true
# Needed for standalone authentication with Atlassian OAuth
ATLASSIAN_OAUTH_CLIENT_ID=<client-id>
ATLASSIAN_OAUTH_CLIENT_SECRET=<client-secret>
# Needed for mock forge storage implementation
FORGE_STORAGE_FILE_PATH=
FORGE_STORAGE_INDEX_FIELD_MAPPING=
See https://developer.atlassian.com/cloud/jira/software/oauth-2-3lo-apps/#enabling-oauth-2-0--3lo- for instructions on creating an OAuth App to use for authentication. The callback url will be http://localhost:4200/callback if your UI module is running locally on port 4200.
Note: If you will be running more than one frontend app locally at the same time, you will need to configure a different OAuth App for each frontend port number (4200, 4201, etc.), and make sure they consistently choose the same port.
index.ts
Rename src/index.js to index.ts and replace contents with:
import { bootstrapExpress, expressDefaults, registerFunctions } from '@valiantys/atlassian-app-backend';
import Resolver from '@forge/resolver';
import * as dotenv from 'dotenv';
import { getForgeFunctions } from './lib/forge-functions';
import { getStandaloneFunctions } from './lib/standalone-functions';
dotenv.config();
const IS_STANDALONE = process.env.IS_STANDALONE === 'true';
if (IS_STANDALONE) {
const port = parseInt(process.env.PORT || '3000');
// OAuth settings
const clientId = process.env.ATLASSIAN_OAUTH_CLIENT_ID || '';
const clientSecret = process.env.ATLASSIAN_OAUTH_CLIENT_SECRET || '';
// Mock Forge storage settings
const filePath = process.env.FORGE_STORAGE_FILE_PATH || '';
const indexFieldMapping = JSON.parse(process.env.FORGE_STORAGE_INDEX_FIELD_MAPPING || '{}');
bootstrapExpress(getStandaloneFunctions(), 'jira', { clientId, clientSecret }, { ...expressDefaults, port }, { indexFieldMapping, filePath });
}
let resolver: Resolver | null = null;
if (!IS_STANDALONE) {
resolver = new Resolver();
registerFunctions(resolver, getForgeFunctions());
}
export const handler = resolver?.getDefinitions();
Example Resolver Function definition files
By abstracting out Forge dependencies into services, we are able to run the same functions in both the deployed Forge environment and in a standalone mode. In the example below, you will see that these functions are defined in the handler-functions.ts file, and depend on environment-specific services that are passed in. These services are initialized when the index file logic calls either the getForgeFunctions() or the getStandaloneFunctions() method based on the IS_STANDALONE environment variable.
forge-functions.ts
import { storage, startsWith, WhereConditions } from '@forge/api';
import { createServerSideForgeProductFetchService, ResolverFunctionMap } from '@valiantys/atlassian-app-backend';
import { handlerFunctions } from './handler-functions';
export function getForgeFunctions(): ResolverFunctionMap {
const productFetchService = createServerSideForgeProductFetchService('user', 'jira');
const forgeStorageService = {
storage,
WhereConditions,
startsWith,
};
// Here you could initialize any other deployed-specific services or config
// to pass to handlerFunctions as needed.
return handlerFunctions(productFetchService, forgeStorageService);
}
standalone-functions.ts
The getStandaloneFunctions() function actually returns a function that is called by the library's express.js application setup. The library code will handle the details of passing in an implementation of a fetch service and the forge storage service. If you are targeting Confluence or Bitbucket instead of Jira, simply change the type parameter you see below.
import { AtlassianProductFetchService, ForgeStorageServiceStandalone, ResolverFunctionFactory } from '@valiantys/atlassian-app-backend';
import { handlerFunctions } from './handler-functions';
export function getStandaloneFunctions(): ResolverFunctionFactory<'jira'> {
return (productFetchService: AtlassianProductFetchService<'jira'>, forgeStorageService: ForgeStorageServiceStandalone) => {
// Here you could initialize any other standalone-specific services or config
// to pass to handlerFunctions as needed.
return handlerFunctions(productFetchService, forgeStorageService);
};
}
handler-functions.ts
This file contains generic function definitions that can be used in either environment. All environment-specific configuration or implementation should be passed in as parameters.
import { AtlassianProductFetchService, ForgeRequest, ForgeStorageService } from '@valiantys/atlassian-app-backend';
// Add any additional environment specific config/services as parameters
// to handlerFunctions()
export function handlerFunctions(productFetchService: AtlassianProductFetchService<'jira'>, forgeStorageService: ForgeStorageService) {
return {
getText: {
function: () => {
return { text: 'Hello world!' };
},
},
/*
Making an Atlassian product API request (@forge/api requestJira)
*/
getIssueTypes: {
function: () =>
productFetchService.fetch<IssueTypeDetails[]>({
url: productFetchService.route`/rest/api/2/issuetype`,
method: 'GET',
}),
},
/**
* Forge storage examples
*/
getMyName: {
function: async () => {
const name = await forgeStorageService.storage.get('myName');
return { name };
},
},
setMyName: {
function: async ({ payload }: ForgeRequest<{ name: string }>) => {
await forgeStorageService.storage.set('myName', payload.name);
const name = await forgeStorageService.storage.get('myName');
return { name };
},
updatesForgeStorage: true, // triggers save of local file when in standalone mode
},
/**
* This example demonstrates how one can access the user's account ID
* in either deployed or standalone mode.
* This context is made available on every backend request.
*/
whoAmI: {
function: async ({ context }: ForgeRequest<void>) => {
console.log(context.accountId);
return { myId: context.accountId };
},
},
};
}
interface IssueTypeDetails {
avatarId: number;
description: string;
hierarchyLevel: number;
iconUrl: string;
id: string;
name: string;
self: string;
subtask: boolean;
}
Add a UI module
See the README file in the @valiantys/atlassian-app-frontend library for instructions on adding a Custom UI module to your app.
Run the app locally (standalone mode)
npm run dev
Forge Storage in standalone mode
When running in standalone mode, the app will be using an in-memory implementation of the Forge storage API, which persists changes to a local json file. When the app starts up it will check for an existing json file and load it into memory. Every time a resolver function completes, the json file will be updated if the function was marked with updatesForgeStorage:true. The location of the storage.json file is controlled by the environment variable FORGE_STORAGE_FILE_PATH, which should be set in your .env file.
Deploy the app
forge deploy
Install
forge install --site <your-site-name>.atlassian.net --product=jira --no-interactive --environment <env-name>
Configure CORS
By default, the standalone backend allows cross-origin requests from http://localhost:4200 and http://localhost:4201. To change this configuration, you may pass in a custom origin configuration to the bootstrapExpress function in index.ts.
Example:
bootstrapExpress(getStandaloneFunctions(), 'jira', { clientId, clientSecret }, { ...expressDefaults, port, corsOptions: { origin: 'http://example.com' } }, { indexFieldMapping, filePath });
You can pass in any CORS options as defined here: https://www.npmjs.com/package/cors#configuration-options
Unit testing
Install jest
npm i -D jest ts-jest @types/jest
Configure Jest
Add a jest.config.js file in the project root directory.
/** @type {import('ts-jest').JestConfigWithTsJest} **/
module.exports = {
modulePathIgnorePatterns: ['<rootDir>/static/'],
testEnvironment: 'node',
transform: {
'^.+.ts?$': ['ts-jest', {}],
},
};
Exclude this file in eslint.config.mjs.
{ignores: ["static/*", "dist/*", "jest.config.js"]},
Example Test
import { handlerFunctions } from './handler-functions';
import { atlassianOAuthJiraFetch, AtlassianProductFetchService, ForgeStorageServiceStandalone, ForgeStorageStandaloneData } from '@valiantys/atlassian-app-backend';
describe('handlerFunctions', () => {
let storage: ForgeStorageStandaloneData;
let forgeStorageService: ForgeStorageServiceStandalone;
let productFetchService: AtlassianProductFetchService<'jira'>;
beforeEach(() => {
// use in-memory forge service for testing
storage = {
secrets: {},
keyValues: {},
entityCollections: {},
};
forgeStorageService = new ForgeStorageServiceStandalone({}, storage);
// Mock out fetch for testing
productFetchService = atlassianOAuthJiraFetch('test-token', 'test-site');
productFetchService.fetch = jest.fn();
});
it('getText should return a text value', () => {
const response = handlerFunctions(productFetchService, forgeStorageService).getText.function();
expect(response).toEqual({
text: 'Hello world!',
});
});
});
Example Application
You will find an examples directory under node_modules/@valiantys/atlassian-app-backend/examples, containing all the examples shown in this README along with more supporting files. The repo for the full example application is also available at https://bitbucket.org/oasisdigital/atlassian-app-examples.
If you would like to clone a template repo to get started more quickly, you can find one at https://bitbucket.org/oasisdigital/atlassian-app-template.
Support
Our issue-tracking board is viewable at https://trello.com/b/aRmPXQjq/atlassian-app-frontend-backend-npm-package-issue-tracker. To file an issue you may send it in an email to [email protected].
You may also contact us with questions at [email protected].