From 3731d2a34be5de9778e37e90974fa8323ee0b24f Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Fri, 13 Oct 2023 00:39:32 -0700 Subject: [PATCH] Nango QBO POC complete With ability to sync QBO data using creds from Nango --- integrations/integration-qbo/QBOClient.ts | 1 + packages/cdk-core/NangoClient.ts | 15 ++++++++-- packages/engine-backend/router/adminRouter.ts | 29 ++++++++++++------- .../engine-backend/router/endUserRouter.ts | 10 +++++-- .../ui/domain-components/ProviderCard.tsx | 2 +- 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/integrations/integration-qbo/QBOClient.ts b/integrations/integration-qbo/QBOClient.ts index adc49d6a..b6b9da82 100644 --- a/integrations/integration-qbo/QBOClient.ts +++ b/integrations/integration-qbo/QBOClient.ts @@ -11,6 +11,7 @@ export const zConfig = z.object({ clientId: z.string(), clientSecret: z.string(), scope: z.string(), + envName: z.enum(['sandbox', 'production']), url: z.string().nullish().describe('For proxies, not typically needed'), verifierToken: z.string().nullish().describe('For webhooks'), }) diff --git a/packages/cdk-core/NangoClient.ts b/packages/cdk-core/NangoClient.ts index 390fd452..f85f9024 100644 --- a/packages/cdk-core/NangoClient.ts +++ b/packages/cdk-core/NangoClient.ts @@ -173,10 +173,12 @@ export const zConnection = zConnectionShort.extend({ raw: z.object({ access_token: z.string(), expires_in: z.number(), - refresh_token_expires_in: z.number(), + expires_at: z.string().datetime(), + /** Refresh token (Only returned if the REFRESH_TOKEN boolean parameter is set to true and the refresh token is available) */ + refresh_token: z.string().nullish(), + refresh_token_expires_in: z.number().nullish(), token_type: z.string(), //'bearer', scope: z.string(), - expires_at: z.string().datetime(), }), }), connection_config: z.record(z.unknown()), @@ -192,6 +194,7 @@ export const zConnection = zConnectionShort.extend({ export const zIntegration = zIntegrationShort.extend({ client_id: z.string(), client_secret: z.string(), + /** comma deliminated scopes with no spaces in between */ scopes: z.string(), app_link: z.string().nullish(), // In practice we only use nango for oauth integrations @@ -214,6 +217,8 @@ export const zUpsertIntegration = zIntegration }) .partial({auth_mode: true}) +export type UpsertIntegration = z.infer + export const endpoints = { get: { '/config': {input: {}, output: z.array(zIntegrationShort)}, @@ -251,7 +256,11 @@ export const endpoints = { '/connection/{connection_id}': { input: { path: z.object({connection_id: z.string()}), - query: z.object({provider_config_key: z.string()}), + query: z.object({ + provider_config_key: z.string(), + force_refresh: z.boolean().optional(), + refresh_token: z.boolean().optional(), + }), }, output: z.undefined(), }, diff --git a/packages/engine-backend/router/adminRouter.ts b/packages/engine-backend/router/adminRouter.ts index d8f0c696..20059862 100644 --- a/packages/engine-backend/router/adminRouter.ts +++ b/packages/engine-backend/router/adminRouter.ts @@ -1,5 +1,7 @@ import {TRPCError} from '@trpc/server' +import type { + UpsertIntegration} from '@usevenice/cdk-core'; import { extractProviderName, handlersLink, @@ -9,7 +11,7 @@ import { zId, zRaw, } from '@usevenice/cdk-core' -import {makeUlid, rxjs, z} from '@usevenice/util' +import {HTTPError, makeUlid, rxjs, z} from '@usevenice/util' import {adminProcedure, trpc} from './_base' @@ -115,15 +117,22 @@ export const adminRouter = trpc.router({ if (provider.metadata?.nangoProvider) { // TODO: Should we use put vs. post? need to fix it up here... // Create nango integration here... - await ctx.nango.put('/config', { - bodyJson: { - provider_config_key: id, - provider: provider.metadata.nangoProvider, - // TODO: gotta fix the typing here... - oauth_client_id: (input.config as any).clientId, - oauth_client_secret: (input.config as any).clientSecret, - oauth_scopes: (input.config as any).scope, - }, + const bodyJson: UpsertIntegration = { + provider_config_key: id, + provider: provider.metadata.nangoProvider, + // TODO: gotta fix the typing here... + oauth_client_id: (input.config as any).clientId, + oauth_client_secret: (input.config as any).clientSecret, + oauth_scopes: (input.config as any).scope, + } + await ctx.nango.put('/config', {bodyJson}).catch((err) => { + if ( + err instanceof HTTPError && + err.response?.data.type === 'unknown_provider_config' + ) { + return ctx.nango.post('/config', {bodyJson}) + } + throw err }) } diff --git a/packages/engine-backend/router/endUserRouter.ts b/packages/engine-backend/router/endUserRouter.ts index ba467772..e0474e3d 100644 --- a/packages/engine-backend/router/endUserRouter.ts +++ b/packages/engine-backend/router/endUserRouter.ts @@ -87,12 +87,18 @@ export const endUserRouter = trpc.router({ .parse(input) const result = await ctx.nango.get('/connection/{connectionId}', { path: {connectionId: resoId}, - query: {provider_config_key: intId}, + query: {provider_config_key: intId, refresh_token: true}, }) return { resourceExternalId: extractId(resoId)[2], - settings: {nango: result}, + settings: { + nango: result, + accessToken: result.credentials.access_token, + accessTokenExpiresAt: result.credentials.expires_at, + refreshToken: result.credentials.raw.refresh_token, + realmId: result.connection_config['realmId'], + }, } satisfies Omit, 'endUserId'> } diff --git a/packages/ui/domain-components/ProviderCard.tsx b/packages/ui/domain-components/ProviderCard.tsx index 8d44bcdc..bebab951 100644 --- a/packages/ui/domain-components/ProviderCard.tsx +++ b/packages/ui/domain-components/ProviderCard.tsx @@ -109,7 +109,7 @@ export const IntegrationCard = ({ // Temporary hack due to presence of labels for plaid. Need better design for ProviderCard and IntegrationCard labels={ // TODO: Fix this hack soon. We should have some kind of mapStandardIntegration method - int.providerName === 'plaid' && int.config?.['envName'] + int.config?.['envName'] ? // eslint-disable-next-line @typescript-eslint/no-base-to-string [`${int.config?.['envName']}`] : []