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(Gong Node): New node #10777

Merged
merged 25 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
735170b
add gong node
feelgood-interface Sep 11, 2024
f8f251d
add gong oAuth2
feelgood-interface Sep 17, 2024
f2fc28e
add tests
feelgood-interface Sep 17, 2024
9fed6fd
review improvements
feelgood-interface Sep 17, 2024
6acfaec
review improvements
feelgood-interface Sep 26, 2024
9deb1ab
lint
feelgood-interface Sep 26, 2024
dbe26b6
fix fallback value
feelgood-interface Sep 30, 2024
6952903
use root metaData for getAll calls
feelgood-interface Oct 1, 2024
0185fc3
handle errors
feelgood-interface Oct 1, 2024
0de1ce2
Merge remote-tracking branch 'upstream/master' into gong-node
feelgood-interface Oct 1, 2024
3c7ff9b
add validation
feelgood-interface Oct 8, 2024
edfd360
flatten metaData
feelgood-interface Oct 8, 2024
660b9f4
fix tests
feelgood-interface Oct 8, 2024
65a8197
improve error message
feelgood-interface Oct 8, 2024
b6f67ff
handle error
feelgood-interface Oct 8, 2024
bcf34b0
use n8n docs
feelgood-interface Oct 15, 2024
a96fb57
use n8n docs
feelgood-interface Oct 15, 2024
5ec52e5
use typeOptions password for accessKey
feelgood-interface Oct 15, 2024
123bbc9
lint reorder import
feelgood-interface Oct 15, 2024
4b30501
cleanup code
feelgood-interface Oct 15, 2024
efc6954
abstract pagination
feelgood-interface Oct 15, 2024
e322e99
abstract error handling
feelgood-interface Oct 15, 2024
30c5d71
throw error if credentials not found
feelgood-interface Oct 15, 2024
44cca86
lint
feelgood-interface Oct 15, 2024
4c3e1bb
fix response for empty user calls, reuse call extraction
feelgood-interface Oct 15, 2024
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
56 changes: 56 additions & 0 deletions packages/nodes-base/credentials/GongApi.credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';

export class GongApi implements ICredentialType {
name = 'gongApi';

displayName = 'Gong API';

documentationUrl = 'https://gong.app.gong.io/settings/api/documentation';
ShireenMissi marked this conversation as resolved.
Show resolved Hide resolved

properties: INodeProperties[] = [
{
displayName: 'Base URL',
name: 'baseUrl',
type: 'string',
default: 'https://api.gong.io',
},
{
displayName: 'Access Key',
name: 'accessKey',
// eslint-disable-next-line n8n-nodes-base/cred-class-field-type-options-password-missing
ShireenMissi marked this conversation as resolved.
Show resolved Hide resolved
type: 'string',
default: '',
},
{
displayName: 'Access Key Secret',
name: 'accessKeySecret',
type: 'string',
default: '',
typeOptions: {
password: true,
},
},
];

authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
auth: {
username: '={{ $credentials.accessKey }}',
password: '={{ $credentials.accessKeySecret }}',
},
},
};

test: ICredentialTestRequest = {
request: {
baseURL: '={{ $credentials.baseUrl.replace(new RegExp("/$"), "") }}',
url: '/v2/users',
},
};
}
59 changes: 59 additions & 0 deletions packages/nodes-base/credentials/GongOAuth2Api.credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { ICredentialType, INodeProperties } from 'n8n-workflow';

export class GongOAuth2Api implements ICredentialType {
name = 'gongOAuth2Api';

extends = ['oAuth2Api'];

displayName = 'Gong OAuth2 API';

documentationUrl = 'https://help.gong.io/docs/create-an-app-for-gong';

properties: INodeProperties[] = [
{
displayName: 'Base URL',
name: 'baseUrl',
type: 'string',
default: 'https://api.gong.io',
},
{
displayName: 'Grant Type',
name: 'grantType',
type: 'hidden',
default: 'authorizationCode',
},
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden',
default: 'https://app.gong.io/oauth2/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden',
default: 'https://app.gong.io/oauth2/generate-customer-token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden',
default:
'api:calls:read:transcript api:provisioning:read api:workspaces:read api:meetings:user:delete api:crm:get-objects api:data-privacy:delete api:crm:schema api:flows:write api:crm:upload api:meetings:integration:status api:calls:read:extensive api:meetings:user:update api:integration-settings:write api:settings:scorecards:read api:stats:scorecards api:stats:interaction api:stats:user-actions api:crm:integration:delete api:calls:read:basic api:calls:read:media-url api:digital-interactions:write api:crm:integrations:read api:library:read api:data-privacy:read api:users:read api:logs:read api:calls:create api:meetings:user:create api:stats:user-actions:detailed api:settings:trackers:read api:crm:integration:register api:provisioning:read-write api:engagement-data:write api:permission-profile:read api:permission-profile:write api:flows:read api:crm-calls:manual-association:read',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden',
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden',
default: 'header',
},
];
}
215 changes: 215 additions & 0 deletions packages/nodes-base/nodes/Gong/GenericFunctions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import type {
DeclarativeRestApiSettings,
IDataObject,
IExecuteFunctions,
IExecutePaginationFunctions,
IExecuteSingleFunctions,
IHttpRequestMethods,
IHttpRequestOptions,
ILoadOptionsFunctions,
IN8nHttpFullResponse,
INodeExecutionData,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';

import get from 'lodash/get';
import toPath from 'lodash/toPath';

ShireenMissi marked this conversation as resolved.
Show resolved Hide resolved
export async function gongApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
query: IDataObject = {},
) {
const authentication = this.getNodeParameter('authentication', 0) as 'accessToken' | 'oAuth2';
const credentialsType = authentication === 'oAuth2' ? 'gongOAuth2Api' : 'gongApi';
const { baseUrl } = await this.getCredentials<{
baseUrl: string;
}>(credentialsType);

const options: IHttpRequestOptions = {
method,
url: baseUrl.replace(new RegExp('/$'), '') + endpoint,
json: true,
headers: {
'Content-Type': 'application/json',
},
body,
qs: query,
};

if (Object.keys(body).length === 0) {
delete options.body;
}

return await this.helpers.requestWithAuthentication.call(this, credentialsType, options);
}

export async function gongApiPaginateRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
query: IDataObject = {},
itemIndex: number = 0,
rootProperty: string | undefined = undefined,
): Promise<any> {
const authentication = this.getNodeParameter('authentication', 0) as 'accessToken' | 'oAuth2';
const credentialsType = authentication === 'oAuth2' ? 'gongOAuth2Api' : 'gongApi';
const { baseUrl } = await this.getCredentials<{
baseUrl: string;
}>(credentialsType);

const options: IHttpRequestOptions = {
method,
url: baseUrl.replace(new RegExp('/$'), '') + endpoint,
json: true,
headers: {
'Content-Type': 'application/json',
},
body,
qs: query,
};

if (Object.keys(body).length === 0) {
delete options.body;
}

const pages = await this.helpers.requestWithAuthenticationPaginated.call(
this,
options,
itemIndex,
{
requestInterval: 340, // Rate limit 3 calls per second
continue: '={{ $response.body.records.cursor }}',
request: {
[method === 'POST' ? 'body' : 'qs']:
'={{ $if($response.body?.records.cursor, { cursor: $response.body.records.cursor }, {}) }}',
url: options.url,
},
},
credentialsType,
);

if (rootProperty) {
let results: IDataObject[] = [];
for (const page of pages) {
const items = page.body[rootProperty];
if (items) {
results = results.concat(items);
}
}
return results;
} else {
return pages.flat();
}
}

export const getCursorPaginator = (rootProperty: string | null = null) => {
ShireenMissi marked this conversation as resolved.
Show resolved Hide resolved
return async function cursorPagination(
this: IExecutePaginationFunctions,
requestOptions: DeclarativeRestApiSettings.ResultOptions,
): Promise<INodeExecutionData[]> {
let executions: INodeExecutionData[] = [];
let responseData: INodeExecutionData[];
let nextCursor: string | undefined = undefined;
const returnAll = this.getNodeParameter('returnAll', true) as boolean;

const extractItems = (page: INodeExecutionData) => {
let items: IDataObject[] = [page.json];
if (rootProperty) {
const paths = toPath(rootProperty);
for (const path of paths) {
items = items.flatMap((x) => get(x, path)) as IDataObject[];
}
}
if (items.length > 0) {
executions = executions.concat(items.map((item) => ({ json: item })));
}
};

do {
(requestOptions.options.body as IDataObject).cursor = nextCursor;
responseData = await this.makeRoutingRequest(requestOptions);
const lastItem = responseData[responseData.length - 1].json;
nextCursor = (lastItem.records as IDataObject)?.cursor as string | undefined;
responseData.forEach(extractItems);
} while (returnAll && nextCursor);

return executions;
};
};

export const getCursorPaginatorCalls = () => {
return async function cursorPagination(
this: IExecutePaginationFunctions,
requestOptions: DeclarativeRestApiSettings.ResultOptions,
): Promise<INodeExecutionData[]> {
let executions: INodeExecutionData[] = [];
let responseData: INodeExecutionData[];
let nextCursor: string | undefined = undefined;
const returnAll = this.getNodeParameter('returnAll', true) as boolean;

const extractItems = (page: INodeExecutionData) => {
let items: IDataObject[] = [page.json];
items = items.flatMap((x) => get(x, 'calls')) as IDataObject[];
if (items.length > 0) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item?.metaData) {
items[i] = {
...(item.metaData as IDataObject),
...item,
};
delete items[i].metaData;
}
}
executions = executions.concat(items.map((item) => ({ json: item })));
}
};

do {
(requestOptions.options.body as IDataObject).cursor = nextCursor;
responseData = await this.makeRoutingRequest(requestOptions);
const lastItem = responseData[responseData.length - 1].json;
nextCursor = (lastItem.records as IDataObject)?.cursor as string | undefined;
responseData.forEach(extractItems);
} while (returnAll && nextCursor);

return executions;
};
};

export async function sendErrorPostReceive(
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject);
}
return data;
}

export function isValidNumberIds(value: number | number[] | string | string[]): boolean {
if (typeof value === 'number') {
return true;
}

if (Array.isArray(value) && value.every((item) => typeof item === 'number')) {
return true;
}

if (typeof value === 'string') {
const parts = value.split(',');
return parts.every((part) => !isNaN(Number(part.trim())));
}

if (Array.isArray(value) && value.every((item) => typeof item === 'string')) {
return true;
}

return false;
}
18 changes: 18 additions & 0 deletions packages/nodes-base/nodes/Gong/Gong.node.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.gong",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development", "Developer Tools"],
"resources": {
"credentialDocumentation": [
{
"url": "https://gong.app.gong.io/settings/api/documentation"
ShireenMissi marked this conversation as resolved.
Show resolved Hide resolved
}
],
"primaryDocumentation": [
{
"url": "https://gong.app.gong.io/settings/api/documentation"
ShireenMissi marked this conversation as resolved.
Show resolved Hide resolved
}
]
}
}
Loading