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/microsoft dynamics connection #625

Merged
merged 3 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/ecommerce/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: "Read data from multiple Ecommerce platforms using a single API"
icon: "star"
---

## List files in a Ecommerce provider using Panora
## List products in a Ecommerce provider using Panora
Copy link
Contributor

Choose a reason for hiding this comment

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

Fix grammatical issue.

Use "an" instead of "a" before "Ecommerce provider".

- ## List products in a Ecommerce provider using Panora
+ ## List products in an Ecommerce provider using Panora
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## List products in a Ecommerce provider using Panora
## List products in an Ecommerce provider using Panora
Tools
LanguageTool

[misspelling] ~7-~7: Use “an” instead of ‘a’ if the following word starts with a vowel sound, e.g. ‘an article’, ‘an hour’.
Context: ..." icon: "star" --- ## List products in a Ecommerce provider using Panora <Check...

(EN_A_VS_AN)


<Check>
We assume for this tutorial that you have a valid Panora API Key, and a
Expand Down Expand Up @@ -32,13 +32,13 @@ icon: "star"
</CodeGroup>
</Step>

<Step title="List files in your Ecommerce:">
<Info>In this example, we will list files in a Ecommerce. Visit other sections of the documentation to find category-specific examples</Info>
<Step title="List products in your Ecommerce:">
<Info>In this example, we will list products in a Ecommerce. Visit other sections of the documentation to find category-specific examples</Info>
<CodeGroup>

```shell curl
curl --request GET \
--url https://api.panora.dev/filestorage/files \
--url https://api.panora.dev/ecommerce/products \
--header 'x-api-key: <api-key>' \
--header 'x-connection-token: <x-connection-token>'
```
Expand All @@ -50,7 +50,7 @@ icon: "star"
apiKey: process.env.API_KEY,
});

const result = await panora.filestorage.files.list({
const result = await panora.ecommerce.products.list({
xConnectionToken: "YOUR_USER_CONNECTION_TOKEN",
});

Expand All @@ -65,7 +65,7 @@ icon: "star"
api_key=os.getenv("API_KEY", ""),
)

res = panora.filestorage.files.list(x_connection_token="YOUR_USER_CONNECTION_TOKEN")
res = panora.ecommerce.products.list(x_connection_token="YOUR_USER_CONNECTION_TOKEN")

print(res)
```
Expand Down
13 changes: 10 additions & 3 deletions packages/api/src/@core/connections/connections.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type StateDataType = {
linkedUserId: string;
providerName: string;
returnUrl?: string;
[key: string]: any;
};

export class BodyDataType {
Expand Down Expand Up @@ -81,15 +82,21 @@ export class ConnectionsController {
}

const stateData: StateDataType = JSON.parse(decodeURIComponent(state));
const { projectId, vertical, linkedUserId, providerName, returnUrl } =
stateData;
const {
projectId,
vertical,
linkedUserId,
providerName,
returnUrl,
resource,
} = stateData;

const service = this.categoryConnectionRegistry.getService(
vertical.toLowerCase(),
);
await service.handleCallBack(
providerName,
{ linkedUserId, projectId, code, otherParams },
{ linkedUserId, projectId, code, otherParams, resource },
'oauth2',
);
if (providerName == 'shopify') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ZendeskConnectionService } from './services/zendesk/zendesk.service';
import { ZohoConnectionService } from './services/zoho/zoho.service';
import { WealthboxConnectionService } from './services/wealthbox/wealthbox.service';
import { AcceloConnectionService } from './services/accelo/accelo.service';
import { MicrosoftDynamicsSalesConnectionService } from './services/microsoftdynamicssales/microsoftdynamicssales.service';

@Module({
imports: [WebhookModule, BullQueueModule],
Expand All @@ -44,6 +45,7 @@ import { AcceloConnectionService } from './services/accelo/accelo.service';
TeamworkConnectionService,
WealthboxConnectionService,
AcceloConnectionService,
MicrosoftDynamicsSalesConnectionService,
],
exports: [CrmConnectionsService],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { EncryptionService } from '@@core/@core-services/encryption/encryption.service';
import { EnvironmentService } from '@@core/@core-services/environment/environment.service';
import { LoggerService } from '@@core/@core-services/logger/logger.service';
import { PrismaService } from '@@core/@core-services/prisma/prisma.service';
import { RetryHandler } from '@@core/@core-services/request-retry/retry.handler';
import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service';
import { ConnectionUtils } from '@@core/connections/@utils';
import {
AbstractBaseConnectionService,
OAuthCallbackParams,
PassthroughInput,
RefreshParams,
} from '@@core/connections/@utils/types';
import { PassthroughResponse } from '@@core/passthrough/types';
import { Injectable } from '@nestjs/common';
import {
AuthStrategy,
CONNECTORS_METADATA,
OAuth2AuthData,
providerToType,
} from '@panora/shared';
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import { ServiceRegistry } from '../registry.service';
import { URLSearchParams } from 'url';
Copy link
Contributor

Choose a reason for hiding this comment

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

Use node: protocol for Node.js built-in modules.

Using the node: protocol is more explicit and signals that the imported module belongs to Node.js.

- import { URLSearchParams } from 'url';
+ import { URLSearchParams } from 'node:url';
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { URLSearchParams } from 'url';
import { URLSearchParams } from 'node:url';
Tools
Biome

[error] 25-25: A Node.js builtin module should be imported with the node: protocol.

Using the node: protocol is more explicit and signals that the imported module belongs to Node.js.
Unsafe fix: Add the node: protocol.

(lint/style/useNodejsImportProtocol)


export type MicrosoftDynamicsSalesOAuthResponse = {
access_token: string;
refresh_token: string;
token_type: string;
expires_in: number;
scope: string;
};

@Injectable()
export class MicrosoftDynamicsSalesConnectionService extends AbstractBaseConnectionService {
private readonly type: string;

constructor(
protected prisma: PrismaService,
private logger: LoggerService,
private env: EnvironmentService,
protected cryptoService: EncryptionService,
private registry: ServiceRegistry,
private cService: ConnectionsStrategiesService,
private connectionUtils: ConnectionUtils,
private retryService: RetryHandler,
) {
super(prisma, cryptoService);
this.logger.setContext(MicrosoftDynamicsSalesConnectionService.name);
this.registry.registerService('microsoftdynamicssales', this);
this.type = providerToType(
'microsoftdynamicssales',
'crm',
AuthStrategy.oauth2,
);
}

async passthrough(
input: PassthroughInput,
connectionId: string,
): Promise<PassthroughResponse> {
try {
const { headers } = input;
const config = await this.constructPassthrough(input, connectionId);

const connection = await this.prisma.connections.findUnique({
where: {
id_connection: connectionId,
},
});

config.headers['Authorization'] = `Basic ${Buffer.from(
`${this.cryptoService.decrypt(connection.access_token)}:`,
).toString('base64')}`;

config.headers = {
...config.headers,
...headers,
};

return await this.retryService.makeRequest(
{
method: config.method,
url: config.url,
data: config.data,
headers: config.headers,
},
'crm.microsoftdynamicssales.passthrough',
config.linkedUserId,
);
} catch (error) {
throw error;
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove redundant catch clause.

The catch clause that only rethrows the original error is redundant and can be removed.

-    } catch (error) {
-      throw error;
-    }
+    }
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
throw error;
}
Tools
Biome

[error] 93-93: The catch clause that only rethrows the original error is redundant.

These unnecessary catch clauses can be confusing. It is recommended to remove them.

(lint/complexity/noUselessCatch)

}
}

async handleCallback(opts: OAuthCallbackParams) {
try {
const { linkedUserId, projectId, code, resource } = opts;
const isNotUnique = await this.prisma.connections.findFirst({
where: {
id_linked_user: linkedUserId,
provider_slug: 'microsoftdynamicssales',
vertical: 'crm',
},
});

const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`;

const CREDENTIALS = (await this.cService.getCredentials(
projectId,
this.type,
)) as OAuth2AuthData;

const formData = new URLSearchParams({
redirect_uri: REDIRECT_URI,
client_id: CREDENTIALS.CLIENT_ID,
client_secret: CREDENTIALS.CLIENT_SECRET,
code: code,
scope: `https://${resource}/.default offline_access`,
grant_type: 'authorization_code',
});
const res = await axios.post(
`https://login.microsoftonline.com/common/oauth2/v2.0/token`,
formData.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
},
},
);
const data: MicrosoftDynamicsSalesOAuthResponse = res.data;
this.logger.log(
'OAuth credentials : microsoftdynamicssales crm ' +
JSON.stringify(data),
Comment on lines +134 to +135
Copy link
Contributor

Choose a reason for hiding this comment

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

Use template literals for string concatenation.

Template literals are preferred over string concatenation for better readability.

-        'OAuth credentials : microsoftdynamicssales crm ' +
-          JSON.stringify(data),
+        `OAuth credentials : microsoftdynamicssales crm ${JSON.stringify(data)}`,
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'OAuth credentials : microsoftdynamicssales crm ' +
JSON.stringify(data),
`OAuth credentials : microsoftdynamicssales crm ${JSON.stringify(data)}`,
Tools
Biome

[error] 134-135: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.

(lint/style/useTemplate)

);

let db_res;
const connection_token = uuidv4();

if (isNotUnique) {
db_res = await this.prisma.connections.update({
where: {
id_connection: isNotUnique.id_connection,
},
data: {
access_token: this.cryptoService.encrypt(data.access_token),
refresh_token: this.cryptoService.encrypt(data.refresh_token),
account_url: `https://${resource}`,
expiration_timestamp: new Date(
new Date().getTime() + Number(data.expires_in) * 1000,
),
status: 'valid',
created_at: new Date(),
},
});
} else {
db_res = await this.prisma.connections.create({
data: {
id_connection: uuidv4(),
connection_token: connection_token,
provider_slug: 'microsoftdynamicssales',
vertical: 'crm',
token_type: 'oauth2',
account_url: `https://${resource}`,
access_token: this.cryptoService.encrypt(data.access_token),
refresh_token: this.cryptoService.encrypt(data.refresh_token),
expiration_timestamp: new Date(
new Date().getTime() + Number(data.expires_in) * 1000,
),
status: 'valid',
created_at: new Date(),
projects: {
connect: { id_project: projectId },
},
linked_users: {
connect: {
id_linked_user: await this.connectionUtils.getLinkedUserId(
projectId,
linkedUserId,
),
},
},
},
});
}
return db_res;
} catch (error) {
throw error;
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove redundant catch clause.

The catch clause that only rethrows the original error is redundant and can be removed.

-    } catch (error) {
-      throw error;
-    }
+    }
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
throw error;
}
Tools
Biome

[error] 189-189: The catch clause that only rethrows the original error is redundant.

These unnecessary catch clauses can be confusing. It is recommended to remove them.

(lint/complexity/noUselessCatch)

}
}
async handleTokenRefresh(opts: RefreshParams) {
try {
const { connectionId, refreshToken, projectId } = opts;
const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`;
const CREDENTIALS = (await this.cService.getCredentials(
projectId,
this.type,
)) as OAuth2AuthData;

const conn = await this.prisma.connections.findUnique({
where: {
id_connection: connectionId,
},
});

const formData = new URLSearchParams({
grant_type: 'refresh_token',
scope: `${conn.account_url}/.default offline_access`,
client_id: CREDENTIALS.CLIENT_ID,
client_secret: CREDENTIALS.CLIENT_SECRET,
refresh_token: this.cryptoService.decrypt(refreshToken),
redirect_uri: REDIRECT_URI,
});

const res = await axios.post(
`https://login.microsoftonline.com/common/oauth2/v2.0/token`,
formData.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
},
},
);
const data: MicrosoftDynamicsSalesOAuthResponse = res.data;
await this.prisma.connections.update({
where: {
id_connection: connectionId,
},
data: {
access_token: this.cryptoService.encrypt(data.access_token),
refresh_token: this.cryptoService.encrypt(data.refresh_token),
expiration_timestamp: new Date(
new Date().getTime() + Number(data.expires_in) * 1000,
),
},
});
this.logger.log('OAuth credentials updated : microsoftdynamicssales ');
} catch (error) {
throw error;
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove redundant catch clause.

The catch clause that only rethrows the original error is redundant and can be removed.

-    } catch (error) {
-      throw error;
-    }
+    }
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
throw error;
}
Tools
Biome

[error] 240-240: The catch clause that only rethrows the original error is redundant.

These unnecessary catch clauses can be confusing. It is recommended to remove them.

(lint/complexity/noUselessCatch)

}
}
}
19 changes: 17 additions & 2 deletions packages/shared/src/authUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ export const constructAuthUrl = async ({ projectId, linkedUserId, providerName,
baseRedirectURL = redirectUriIngress.value!;
}
const encodedRedirectUrl = encodeURIComponent(`${baseRedirectURL}/connections/oauth/callback`);
const state = encodeURIComponent(JSON.stringify({ projectId, linkedUserId, providerName, vertical, returnUrl }));
let state = encodeURIComponent(JSON.stringify({ projectId, linkedUserId, providerName, vertical, returnUrl }));
if (providerName == 'microsoftdynamicssales') {
state = encodeURIComponent(JSON.stringify({ projectId, linkedUserId, providerName, vertical, returnUrl, resource: additionalParams!.end_user_domain }));
Copy link
Contributor

Choose a reason for hiding this comment

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

Avoid non-null assertion.

Using non-null assertion (!) can lead to runtime errors. Replace it with optional chaining (?.) to ensure safety.

-    state = encodeURIComponent(JSON.stringify({ projectId, linkedUserId, providerName, vertical, returnUrl, resource: additionalParams!.end_user_domain }));
+    state = encodeURIComponent(JSON.stringify({ projectId, linkedUserId, providerName, vertical, returnUrl, resource: additionalParams?.end_user_domain }));
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
state = encodeURIComponent(JSON.stringify({ projectId, linkedUserId, providerName, vertical, returnUrl, resource: additionalParams!.end_user_domain }));
state = encodeURIComponent(JSON.stringify({ projectId, linkedUserId, providerName, vertical, returnUrl, resource: additionalParams?.end_user_domain }));
Tools
Biome

[error] 60-60: Forbidden non-null assertion.

Unsafe fix: Replace with optional chain operator ?. This operator includes runtime checks, so it is safer than the compile-only non-null assertion operator

(lint/style/noNonNullAssertion)

}
// console.log('State : ', JSON.stringify({ projectId, linkedUserId, providerName, vertical, returnUrl }));
// console.log('encodedRedirect URL : ', encodedRedirectUrl);
// const vertical = findConnectorCategory(providerName);
Expand Down Expand Up @@ -166,7 +169,19 @@ const handleOAuth2Url = async (input: HandleOAuth2Url) => {
if (needsScope(providerName, vertical) && scopes) {
if(providerName === 'slack') {
params += `&scope=&user_scope=${encodeURIComponent(scopes)}`;
} else {
} else if (providerName == 'microsoftdynamicssales') {
const url = new URL(BASE_URL);
// Extract the base URL without parameters
const base = url.origin + url.pathname;
// Extract the resource parameter
const resource = url.searchParams.get('resource');
BASE_URL = base;
let b = `https://${resource}/.default`;
b += (' offline_access');
console.log("scopes is "+ b)
console.log("BASE URL is "+ BASE_URL)
Comment on lines +179 to +182
Copy link
Contributor

Choose a reason for hiding this comment

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

Use template literals for string concatenation.

Template literals are preferred over string concatenation for better readability.

-      let b = `https://${resource}/.default`;
-      b += (' offline_access'); 
-      console.log("scopes is "+ b)
-      console.log("BASE URL is "+ BASE_URL)
+      let b = `https://${resource}/.default offline_access`;
+      console.log(`scopes is ${b}`);
+      console.log(`BASE URL is ${BASE_URL}`);
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let b = `https://${resource}/.default`;
b += (' offline_access');
console.log("scopes is "+ b)
console.log("BASE URL is "+ BASE_URL)
let b = `https://${resource}/.default offline_access`;
console.log(`scopes is ${b}`);
console.log(`BASE URL is ${BASE_URL}`);
Tools
Biome

[error] 181-181: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.

(lint/style/useTemplate)


[error] 182-182: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.

(lint/style/useTemplate)

params += `&scope=${encodeURIComponent(b)}`;
}else {
params += `&scope=${encodeURIComponent(scopes)}`;
}
}
Expand Down
15 changes: 9 additions & 6 deletions packages/shared/src/connectors/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,18 +203,21 @@ export const CONNECTORS_METADATA: ProvidersConfig = {
strategy: AuthStrategy.oauth2
}
},
'microsoft_dynamics_sales': {
scopes: '',
'microsoftdynamicssales': {
scopes: 'offline_access',
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a more descriptive scope.

The scope 'offline_access' has been added. While this is correct, consider adding more specific scopes that align with the permissions required by the Microsoft Dynamics Sales API for better security and functionality.

- scopes: 'offline_access',
+ scopes: 'offline_access Dynamics.CRM.read Dynamics.CRM.write',
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
scopes: 'offline_access',
scopes: 'offline_access Dynamics.CRM.read Dynamics.CRM.write',

urls: {
docsUrl: '',
authBaseUrl: '',
apiUrl: '',
authBaseUrl: (orgName) => `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?resource=${orgName}`,
apiUrl: `/api/data/v9.2`,
},
logoPath: 'https://play-lh.googleusercontent.com/EMobDJKabP1eY_63QHgPS_-TK3eRfxXaeOnERbcRaWAw573iaV74pXS9xOv997dRZtM',
logoPath: 'https://play-lh.googleusercontent.com/MC_Aoa7rlMjGtcgAdiLJGeIm3-kpVw7APQmQUrUZtXuoZokiqVOJqR-bTu7idJBD8g',
description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users',
active: false,
options: {
end_user_domain: true
},
authStrategy: {
strategy: AuthStrategy.api_key
strategy: AuthStrategy.oauth2
}
},
'nutshell': {
Expand Down
Loading