This guide will walk you through the process of connecting Graphweaver to the Xero API. An example of a Xero integration can be found on the Graphweaver Github.
Prerequisites
Before you begin, ensure that you have the following prerequisites:
- Node.js 18 or greater installed
pnpm
version 8 or greater installed
Step 1: Obtain Xero API Credentials
Before you start, you'll need to obtain API credentials from Xero. These include a client_id
, client_secret
, redirect_uri
. Refer to the Xero developer documentation for details on obtaining these credentials.
Step 2: Project Initialisation
Create a new Graphweaver project by running the following command:
npx graphweaver@latest init
Follow the prompts to set up your project. Provide a name when prompted, and select the REST backend for connecting to Xero.
Install the dependencies in your project and install xero-node
:
cd your-project-name
pnpm i
pnpm i xero-node
Ensure your Graphweaver instance starts with:
pnpm start
Step 3: Configure Xero API Connection
Create a file named .env
in your project directory and configure the connection details for the Xero API:
XERO_CLIENT_ID="[value from Xero]"
XERO_CLIENT_SECRET="[value from Xero]"
XERO_CLIENT_REDIRECT_URIS="http://localhost:9000/xero-auth-code"
Step 4: Create Xero Entities
xero-node
provides classes for the entities which we will extend for our GraphQL entities. For the Account
entity, we’ll import Account
as XeroAccount, and AccountType
from xero-node
and define the following fields for our GraphQL schema. See the Decorators docs for more details.
import {
AdminUISettings,
GraphQLEntity,
RelationshipField,
SummaryField,
Field,
ID,
ObjectType,
registerEnumType,
} from '@exogee/graphweaver';
import { Account as XeroAccount, AccountType } from 'xero-node';
import { Tenant } from '../tenant';
import { AccountBackendProvider } from './';
registerEnumType(AccountType, { name: 'AccountType' });
@Entity('Account', {
provider: AccountBackendProvider
})
export class Account extends GraphQLEntity<XeroAccount> {
public dataEntity!: XeroAccount & { tenantId: string };
@Field(() => ID)
id!: string;
@Field(() => String, { nullable: true })
code?: string;
@SummaryField()
@Field(() => String, { nullable: true })
name?: string;
@Field(() => AccountType, { nullable: true })
type?: AccountType;
@AdminUISettings({
hideFromFilterBar: true,
})
@Field(() => String)
tenantId!: string;
@RelationshipField<Account>(() => Tenant, { id: 'tenantId' })
tenant!: Tenant;
}
Step 5: Create Data Provider
To access our Xero accounts we need to handle Xero Tenants to query the Xero API. Let’s create a helper function to handle each tenant.
export const forEachTenant = async <T = unknown>(
xero: XeroClient,
callback: ForEachTenantCallback<T>,
filter?: Record<string, any>
): Promise<WithTenantId<T>[]> => {
if (!xero.tenants.length) await xero.updateTenants(false);
const [tenantFilter] = splitFilter(filter);
const filteredTenants = tenantFilter
? xero.tenants.filter(inMemoryFilterFor(tenantFilter)) // this filter function can be found in the Xero Example code
: xero.tenants;
const results = await Promise.all(
filteredTenants.map(async (tenant) => {
const result = (await callback(tenant)) as WithTenantId<T>;
// We should go ahead and doctor up the result(s) with a tenantId,
// as Xero never adds it, but we need it on everything we're doing
// a forEachTenant on.
if (Array.isArray(result)) {
result.forEach((element) => (element.tenantId = tenant.tenantId));
} else {
result.tenantId = tenant.tenantId;
}
return result;
})
);
// Now we have a two dimensional array, which is an array for each tenant. Let's flatten it.
return results.flat() as WithTenantId<T>[];
};
Now we can create resolvers for our entities with the Xero API. Use the provided XeroBackendProvider
and implement the Provider methods. Our find
function gets all the accounts by our tenants, and passes sorting and filtering info into the xero.accountingApi.getAccounts
method.
import { Filter, Sort } from '@exogee/graphweaver';
import { XeroBackendProvider } from '@exogee/graphweaver-xero';
import { forEachTenant, offsetAndLimit, orderByToString, splitFilter } from '../../utils';
import { Account } from './entity';
import { Account as XeroAccount } from 'xero-node';
const defaultSort: Record<string, Sort> = { ['name']: Sort.ASC };
export const AccountBackendProvider = new XeroBackendProvider('Account', {
find: async ({ xero, filter, order, limit, offset }) => {
const fullSet = await forEachTenant<XeroAccount>(
xero,
async (tenant) => {
const sortFields = order ?? defaultSort;
const [_, remainingFilter] = splitFilter(filter);
const {
body: { accounts },
} = await xero.accountingApi.getAccounts(
tenant.tenantId,
undefined,
xeroFilterFrom(remainingFilter),
orderByToString(sortFields)
);
for (const account of accounts) {
(account as XeroAccount & { id: string }).id = account.accountID;
}
return accounts;
},
filter
);
// (filter) -> order -> limit/offset
return offsetAndLimit(fullSet, offset, limit);
},
});
The definitions for all of the sorting, filtering, and offsetAndLimiting can be found in the Xero Graphweaver Example.
Step 6: Authenticate with Xero
Now we need a way to authenticate with Xero from the dashboard. The @exogee/graphweaver-xero
includes an apollo plugin which redirects user’s who first land on the admin UI to Xero to login. The XeroAuthApolloPlugin
is included when instantiating the Graphweaver instance.
import 'reflect-metadata';
import Graphweaver from '@exogee/graphweaver-server';
import './schema';
import { XeroAuthApolloPlugin } from '@exogee/graphweaver-xero';
const graphweaver = new Graphweaver({
apolloServerOptions: {
plugins: [XeroAuthApolloPlugin],
},
});
exports.handler = graphweaver.handler();
Finally we need to receive the code from Xero from our XERO_CLIENT_REDIRECT_URIS
. We can create a component that picks up the code from the URL like this:
import { useSearchParams, Navigate } from 'react-router-dom';
export const XeroAuthCodeReceiver = () => {
const [searchParams] = useSearchParams();
// We need to set here on initial render so that we definitely capture the code if we're receiving an auth redirect.
// Otherwise we get an extra loop through the OAuth flow because local storage gets set too late.
if (
!searchParams.has('code') ||
!searchParams.has('scope') ||
!searchParams.has('session_state')
) {
return (
<p>
Error: Invalid response from Xero, expected to have a code, scope and session_state in the
url.
</p>
);
}
// Ok, we're definitely receiving an auth redirect from Xero or something that acts like Xero.
// Let's read the code and put it in local storage so we can send it to the server.
localStorage.setItem('graphweaver-auth', window.location.href);
// Ok, now that we've saved it we're good to go.
return <Navigate to="/" />;
};
And we place this in our router at the route that matches our XERO_CLIENT_REDIRECT_URIS
in this case /xero-auth-code
.
export const customPages = {
routes: () => [
{
// This is where Xero sends us back to after the OAuth flow.
// Its job is to read the code and store it in local storage, then
// redirect back to /.
path: '/xero-auth-code',
element: <XeroAuthCodeReceiver />,
},
{
path: 'xero-dashboard',
element: <DefaultLayout />,
children: [
{
index: true,
element: <AllCompanies />,
},
{
path: ':tenantId',
element: <SingleCompany />,
},
],
},
],
...
Conclusion
Congratulations! You've successfully connected Graphweaver to the Xero API. You can now explore and interact with Xero data using GraphQL queries.
Feel free to customize this guide based on the specifics of your Xero API and Graphweaver project configuration. For more information on connecting multiple data sources, refer to the Graphweaver Documentation.