Yes, typeguards are still necessary - the benefit of this approach is more the “what am I working with?” as Intellisense and similar can recognise the API calls and responses and guide you through writing your code.
I couldn’t leave this well enough alone. A custom fetch was the way. It turned out to be much simpler than I expected - almost too simple.
First, for client-side fetch, for example, a project settings page:
import React, { useEffect, useState } from 'react';
import ForgeReconciler, { Text } from '@forge/react';
import { RequestInit } from '@forge/api';
import { requestJira } from '@forge/bridge';
import createClient from "openapi-fetch";
import type { paths, components } from '../../schemas/jira-cloud-platform-api';
function asUserJiraFetch (input: Request, options?: RequestInit): Promise<Response> {
const url = new URL(input.url);
return requestJira(url.pathname, {
...options,
method: input.method,
headers: { ...options?.headers, ...Object.fromEntries(input.headers) }
});
};
const jiraUserClient = createClient<paths>({ fetch: asUserJiraFetch });
async function getProjectIssueTypesAsUser(projectId: string): Promise<components['schemas']['IssueTypeWithStatus'][]> {
const { data, error } = await jiraUserClient.GET("/rest/api/3/project/{projectIdOrKey}/statuses",
{ params: { path: { projectIdOrKey: projectId}}});
if (error) { throw new Error(error); }
if (!data || data.length === 0) { throw new Error(`No issue types in project ${projectId}`); }
return data;
}
const App = () => {
const context = useProductContext();
const [asUserIssueTypes, setUserIssueTypes] = useState<components['schemas']['IssueTypeWithStatus'][] | null>(null);
useEffect(() => {
getProjectIssueTypesAsUser(context['extension']['project']['id'])
.then((result) => {
setUserIssueTypes(result as components['schemas']['IssueTypeWithStatus'][])
});
}, []);
return (
<>
<Text>Project issue types</Text>
<Text>{asUserIssueTypes ? `As a user, I see that this project has ${asUserIssueTypes.length} issue types.` : 'Loading...'}</Text>
</>
);
};
ForgeReconciler.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
For resolvers on the Forge backend, it’s much the same - however, because openapi-fetch manipulates the path, for example to bring in the {projectIdOrKey}
path variable, Forge won’t accept the path with route tagging template and I need to use assumeTrustedRoute
:
import Resolver from '@forge/resolver';
import api, { assumeTrustedRoute, RequestInit, APIResponse } from '@forge/api';
import createClient from "openapi-fetch";
import type { paths, components } from '../../schemas/jira-cloud-platform-api';
function asAppJiraFetch (input: Request, options?: RequestInit): Promise<Response> {
const url = new URL(input.url);
const apiResponse: Promise<APIResponse> = api.asApp().requestJira(
assumeTrustedRoute(url.pathname), { ...options, method: input.method,
headers: { ...options?.headers, ...Object.fromEntries(input.headers) }
});
return apiResponse as Promise<Response>
};
const jiraAppClient = createClient<paths>({
baseUrl: "https://example.com/", // For fetch library compliance
fetch: asAppJiraFetch
});
const resolver = new Resolver();
resolver.define('getProjectIssueTypesAsApp', async (req): Promise<components['schemas']['IssueTypeWithStatus'][]> => {
if (!req.context || !req.context['extension']) {
throw new Error("App context not found");
}
if (!req.context['extension']['project']['id']) {
throw new Error("Project ID not found in app context");
}
const { data, error } = await jiraAppClient.GET("/rest/api/3/project/{projectIdOrKey}/statuses",
{ params: { path: { projectIdOrKey: req.context['extension']['project']['id']}}});
if (error) { throw new Error(error); }
if (!data || data.length === 0) { throw new Error(`No issue types in project ${req.context['extension']['project']['id']}`); }
return data;
});
export const handler = resolver.getDefinitions();
Finally, I had to replace “/rest/api/2” in the Jira Cloud Platform OpenAPI schema with “/rest/api/3” to get it working with Forge’s requestJira fetch clients.
This approach should serve me in rapidly developing my Forge app. I’ll likely sometimes need to call the requestProduct interactions directly, forgoing typechecking, but I’m not prevented from doing that on a case-by-case basis.