Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(openIntProxyLink): Adding Openint proxy link #39

Merged
merged 15 commits into from
Dec 4, 2024
1 change: 1 addition & 0 deletions packages/fetch-links/links/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {Link} from '../link.js'
export * from './axiosLink.js'
export * from './corsLink.js'
export * from './oauthLink.js'
export * from './openIntProxyLink.js'
// codegen:end

// MARK: Built-in links
Expand Down
88 changes: 88 additions & 0 deletions packages/fetch-links/links/openIntProxyLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {Link} from '../link.js'
import {mergeHeaders, modifyRequest} from '../modifyRequestResponse.js'

interface OpenIntProxyHeaders {
authorization?: `Bearer ${string}`
'x-apikey'?: string
'x-resource-id': string
}

export type OpenIntProxyLinkOptions = {
token?: string
apiKey?: string
resourceId?: string
endUserId?: string
connectorName?: string
}

export function validateOpenIntProxyLinkOptions(
options: OpenIntProxyLinkOptions,
): boolean {
const {token, apiKey, resourceId, endUserId, connectorName} = options ?? {}

const hasToken = !!token
const hasApiKey = !!apiKey
const hasResourceId = !!resourceId
const hasEndUserId = !!endUserId
const hasConnectorName = !!connectorName

const expectsAuthProxy =
hasToken || hasApiKey || hasResourceId || hasEndUserId || hasConnectorName
if (
expectsAuthProxy &&
!(
(hasToken && hasResourceId) ||
(hasToken && hasConnectorName) ||
(hasApiKey && hasResourceId) ||
(hasApiKey && hasEndUserId && hasConnectorName)
)
) {
throw new Error(
'Invalid configuration for proxy authentication. You must provide one of the following combinations: ' +
'1) token AND resourceId, ' +
'2) token AND connectorName, ' +
'3) apiKey AND resourceId, ' +
'4) apiKey AND endUserId AND connectorName, ' +
'or none of these options and instead authenticate directly.',
)
}
return expectsAuthProxy
}

interface OpenIntProxyHeaders {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OpenIntProxyHeaders interface is defined twice in this file. Consider removing the duplicate definition to avoid confusion.

authorization?: `Bearer ${string}`
'x-apikey'?: string
'x-resource-id': string
'x-resource-end-user-id'?: string
'x-resource-connector-name'?: string
}

// TODO: Test that I'm actally working!
export function openIntProxyLink(opts: OpenIntProxyLinkOptions): Link {
validateOpenIntProxyLinkOptions(opts)
const {apiKey, token, resourceId, endUserId, connectorName} = opts

const headers = {
['x-apikey']: apiKey || token || '',
['x-resource-id']: resourceId || '',
['x-resource-end-user-id']: endUserId || '',
['x-resource-connector-name']: connectorName || '',
} satisfies OpenIntProxyHeaders

return async (req, next) => {
const baseUrl = getBaseUrl(req.url)
const res = await next(
modifyRequest(req, {
url: req.url.replace(baseUrl, 'https://app.openint.dev/proxy/'),
headers: mergeHeaders(req.headers, headers, {}),
body: req.body,
}),
)
return res
}
}

function getBaseUrl(urlStr: string) {
const url = new URL(urlStr)
return `${url.protocol}//${url.host}/`
}
13 changes: 13 additions & 0 deletions packages/runtime/createClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import type {PathsWithMethod} from 'openapi-typescript-helpers'
import {
applyLinks,
fetchLink,
openIntProxyLink,
OpenIntProxyLinkOptions,
validateOpenIntProxyLinkOptions,
type HTTPMethod,
type Link,
} from '@opensdks/fetch-links'
Expand All @@ -14,6 +17,7 @@ type _ClientOptions = NonNullable<Parameters<typeof _createClient>[0]>

export interface ClientOptions extends _ClientOptions {
links?: Link[] | ((defaultLinks: Link[]) => Link[])
auth?: OpenIntProxyLinkOptions
}

export type OpenAPIClient<Paths extends {}> = ReturnType<
Expand All @@ -31,8 +35,17 @@ export function createClient<Paths extends {}>({
links: _links = defaultLinks,
...clientOptions
}: ClientOptions = {}) {
// Validate configuration options
const expectsAuthProxy = validateOpenIntProxyLinkOptions(
clientOptions.auth ?? {},
)

const links = typeof _links === 'function' ? _links(defaultLinks) : _links

if (expectsAuthProxy) {
links.push(openIntProxyLink(clientOptions.auth ?? {}))
}

const customFetch: typeof fetch = (url, init) =>
applyLinks(new Request(url, init), links)
const client = _createClient<Paths>({...clientOptions, fetch: customFetch})
Expand Down
53 changes: 51 additions & 2 deletions sdks/sdk-qbo/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import {initSDK} from '@opensdks/runtime'
import qboSdkDef from './index.js'

const realmId = process.env['QBO_REALM_ID']!
const accessToken = process.env['QBO_ACCESS_TOKEN']!
const maybeTest = realmId ? test : test.skip

maybeTest('get QBO company', async () => {
maybeTest('get QBO company directly with access token', async () => {
const qbo = initSDK(qboSdkDef, {
realmId,
envName: 'sandbox',
accessToken: process.env['QBO_ACCESS_TOKEN']!,
auth: {
accessToken: accessToken,
},
})

const res = await qbo.GET('/companyinfo/{id}', {
Expand All @@ -19,3 +22,49 @@ maybeTest('get QBO company', async () => {
expect(res.response.status).toEqual(200)
expect(res.data.CompanyInfo.CompanyName).toEqual('Sandbox Company_US_1')
})

const resourceId = process.env['QBO_RESOURCE_ID']!
const apiKey = process.env['QBO_API_KEY']!
maybeTest(
'get QBO company via proxy with api key and resource id',
async () => {
const qbo = initSDK(qboSdkDef, {
realmId,
envName: 'sandbox',
auth: {
apiKey,
resourceId,
},
})

const res = await qbo.GET('/companyinfo/{id}', {
params: {path: {id: realmId}},
})

expect(res.response.status).toEqual(200)
expect(res.data.CompanyInfo.CompanyName).toEqual('Sandbox Company_US_1')
},
)

const connectorName = 'qbo'
const token = process.env['QBO_TOKEN']!
maybeTest(
'get QBO company via proxy with token and connector name',
async () => {
const qbo = initSDK(qboSdkDef, {
realmId,
envName: 'sandbox',
auth: {
token,
connectorName,
},
})

const res = await qbo.GET('/companyinfo/{id}', {
params: {path: {id: realmId}},
})

expect(res.response.status).toEqual(200)
expect(res.data.CompanyInfo.CompanyName).toEqual('Sandbox Company_US_1')
},
)
8 changes: 6 additions & 2 deletions sdks/sdk-qbo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ export type QBOSDKTypes = SDKTypes<
qboOasTypes,
ClientOptions & {
realmId: string
accessToken: string
envName: 'sandbox' | 'production'
auth?: {
accessToken?: string
}
}
>

Expand All @@ -21,7 +23,8 @@ export type QBOSDKTypes = SDKTypes<
export const qboSdkDef = {
types: {} as QBOSDKTypes,
oasMeta: qboOasMeta,
createClient: (ctx, {realmId, accessToken, envName, ...options}) => {
createClient: (ctx, {realmId, envName, ...options}) => {
const {accessToken} = options.auth ?? {}
const client = ctx.createClient({
...options,
baseUrl: qboOasMeta.servers
Expand All @@ -33,6 +36,7 @@ export const qboSdkDef = {
accept: 'application/json',
...options.headers,
},
auth: options.auth,
})
function query(
query: string,
Expand Down
Loading