feat(Gong Node): New node (#10777)

This commit is contained in:
feelgood-interface 2024-10-16 11:23:09 +02:00 committed by GitHub
parent aa3c0dd226
commit 785b47feb3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 3354 additions and 16 deletions

View file

@ -0,0 +1,58 @@
import type {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class GongApi implements ICredentialType {
name = 'gongApi';
displayName = 'Gong API';
documentationUrl = 'gong';
properties: INodeProperties[] = [
{
displayName: 'Base URL',
name: 'baseUrl',
type: 'string',
default: 'https://api.gong.io',
},
{
displayName: 'Access Key',
name: 'accessKey',
type: 'string',
typeOptions: {
password: true,
},
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',
},
};
}

View file

@ -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 = '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',
},
];
}

View file

@ -0,0 +1,227 @@
import get from 'lodash/get';
import type {
DeclarativeRestApiSettings,
IDataObject,
IExecuteFunctions,
IExecutePaginationFunctions,
IExecuteSingleFunctions,
IHttpRequestMethods,
IHttpRequestOptions,
ILoadOptionsFunctions,
IN8nHttpFullResponse,
INodeExecutionData,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
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();
}
}
const getCursorPaginator = (
extractItems: (items: INodeExecutionData[]) => INodeExecutionData[],
) => {
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;
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;
executions = executions.concat(extractItems(responseData));
} while (returnAll && nextCursor);
return executions;
};
};
export const extractCalls = (items: INodeExecutionData[]): INodeExecutionData[] => {
const calls: IDataObject[] = items.flatMap((item) => get(item.json, 'calls') as IDataObject[]);
return calls.map((call) => {
const { metaData, ...rest } = call ?? {};
return { json: { ...(metaData as IDataObject), ...rest } };
});
};
export const extractUsers = (items: INodeExecutionData[]): INodeExecutionData[] => {
const users: IDataObject[] = items.flatMap((item) => get(item.json, 'users') as IDataObject[]);
return users.map((user) => ({ json: user }));
};
export const getCursorPaginatorCalls = () => {
return getCursorPaginator(extractCalls);
};
export const getCursorPaginatorUsers = () => {
return getCursorPaginator(extractUsers);
};
export async function handleErrorPostReceive(
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) {
const { resource, operation } = this.getNode().parameters;
if (resource === 'call') {
if (operation === 'get') {
if (response.statusCode === 404) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required call doesn't match any existing one",
description: "Double-check the value in the parameter 'Call to Get' and try again",
});
}
} else if (operation === 'getAll') {
if (response.statusCode === 404) {
const primaryUserId = this.getNodeParameter('filters.primaryUserIds', {}) as IDataObject;
if (Object.keys(primaryUserId).length !== 0) {
return [{ json: {} }];
}
} else if (response.statusCode === 400 || response.statusCode === 500) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
description: 'Double-check the value(s) in the parameter(s)',
});
}
}
} else if (resource === 'user') {
if (operation === 'get') {
if (response.statusCode === 404) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required user doesn't match any existing one",
description: "Double-check the value in the parameter 'User to Get' and try again",
});
}
} else if (operation === 'getAll') {
if (response.statusCode === 404) {
const userIds = this.getNodeParameter('filters.userIds', '') as string;
if (userIds) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The Users IDs don't match any existing user",
description: "Double-check the values in the parameter 'Users IDs' and try again",
});
}
}
}
}
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;
}

View file

@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.gong",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development", "Developer Tools"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.gong/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.gong/"
}
]
}
}

View file

@ -0,0 +1,171 @@
import {
NodeConnectionType,
type IDataObject,
type ILoadOptionsFunctions,
type INodeListSearchItems,
type INodeListSearchResult,
type INodeType,
type INodeTypeDescription,
} from 'n8n-workflow';
import { callFields, callOperations, userFields, userOperations } from './descriptions';
import { gongApiRequest } from './GenericFunctions';
export class Gong implements INodeType {
description: INodeTypeDescription = {
displayName: 'Gong',
name: 'gong',
icon: 'file:gong.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with Gong API',
defaults: {
name: 'Gong',
},
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
credentials: [
{
name: 'gongApi',
required: true,
displayOptions: {
show: {
authentication: ['accessToken'],
},
},
},
{
name: 'gongOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
],
requestDefaults: {
baseURL: '={{ $credentials.baseUrl.replace(new RegExp("/$"), "") }}',
},
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'accessToken',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Call',
value: 'call',
},
{
name: 'User',
value: 'user',
},
],
default: 'call',
},
...callOperations,
...callFields,
...userOperations,
...userFields,
],
};
methods = {
listSearch: {
async getCalls(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const query: IDataObject = {};
if (paginationToken) {
query.cursor = paginationToken;
}
const responseData = await gongApiRequest.call(this, 'GET', '/v2/calls', {}, query);
const calls: Array<{
id: string;
title: string;
}> = responseData.calls;
const results: INodeListSearchItems[] = calls
.map((c) => ({
name: c.title,
value: c.id,
}))
.filter(
(c) =>
!filter ||
c.name.toLowerCase().includes(filter.toLowerCase()) ||
c.value?.toString() === filter,
)
.sort((a, b) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
return 0;
});
return { results, paginationToken: responseData.records.cursor };
},
async getUsers(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const query: IDataObject = {};
if (paginationToken) {
query.cursor = paginationToken;
}
const responseData = await gongApiRequest.call(this, 'GET', '/v2/users', {}, query);
const users: Array<{
id: string;
emailAddress: string;
firstName: string;
lastName: string;
}> = responseData.users;
const results: INodeListSearchItems[] = users
.map((u) => ({
name: `${u.firstName} ${u.lastName} (${u.emailAddress})`,
value: u.id,
}))
.filter(
(u) =>
!filter ||
u.name.toLowerCase().includes(filter.toLowerCase()) ||
u.value?.toString() === filter,
)
.sort((a, b) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
return 0;
});
return { results, paginationToken: responseData.records.cursor };
},
},
};
}

View file

@ -0,0 +1,603 @@
import type {
IDataObject,
IExecuteSingleFunctions,
IHttpRequestOptions,
IN8nHttpFullResponse,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import {
getCursorPaginatorCalls,
gongApiPaginateRequest,
isValidNumberIds,
handleErrorPostReceive,
extractCalls,
} from '../GenericFunctions';
export const callOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['call'],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Retrieve data for a specific call',
routing: {
request: {
method: 'POST',
url: '/v2/calls/extensive',
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [handleErrorPostReceive],
},
},
action: 'Get call',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve a list of calls',
routing: {
request: {
method: 'POST',
url: '/v2/calls/extensive',
body: {
filter: {},
},
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [handleErrorPostReceive],
},
},
action: 'Get many calls',
},
],
default: 'getAll',
},
];
const getFields: INodeProperties[] = [
{
displayName: 'Call to Get',
name: 'call',
default: {
mode: 'list',
value: '',
},
displayOptions: {
show: {
resource: ['call'],
operation: ['get'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getCalls',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
placeholder: 'e.g. 7782342274025937895',
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '[0-9]{1,20}',
errorMessage: 'Not a valid Gong Call ID',
},
},
],
},
{
displayName: 'By URL',
name: 'url',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/[a-zA-Z0-9-]+\\.app\\.gong\\.io\\/call\\?id=([0-9]{1,20})',
},
placeholder: 'e.g. https://subdomain.app.gong.io/call?id=7782342274025937895',
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/[a-zA-Z0-9-]+\\.app\\.gong\\.io\\/call\\?id=([0-9]{1,20})',
errorMessage: 'Not a valid Gong URL',
},
},
],
},
],
required: true,
routing: {
send: {
type: 'body',
property: 'filter.callIds',
propertyInDotNotation: true,
value: '={{ [$value] }}',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'calls',
},
},
],
},
},
type: 'resourceLocator',
},
{
displayName: 'Options',
name: 'options',
default: {},
displayOptions: {
show: {
resource: ['call'],
operation: ['get'],
},
},
options: [
{
displayName: 'Call Data to Include',
name: 'properties',
type: 'multiOptions',
default: [],
description:
'The Call properties to include in the returned results. Choose from a list, or specify IDs using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
options: [
{
name: 'Action Items',
value: 'pointsOfInterest',
description: 'Call points of interest',
},
{
name: 'Audio and Video URLs',
value: 'media',
description: 'Audio and video URL of the call. The URLs will be available for 8 hours.',
},
{
name: 'Brief',
value: 'brief',
description: 'Spotlight call brief',
routing: {
send: {
type: 'body',
property: 'contentSelector.exposedFields.content.brief',
propertyInDotNotation: true,
value: '={{ $value }}',
},
},
},
{
name: 'Comments',
value: 'publicComments',
description: 'Public comments made for this call',
},
{
name: 'Highlights',
value: 'highlights',
description: 'Call highlights',
},
{
name: 'Keypoints',
value: 'keyPoints',
description: 'Key points of the call',
},
{
name: 'Outcome',
value: 'callOutcome',
description: 'Outcome of the call',
},
{
name: 'Outline',
value: 'outline',
description: 'Call outline',
},
{
name: 'Participants',
value: 'parties',
description: 'Information about the participants of the call',
},
{
name: 'Structure',
value: 'structure',
description: 'Call agenda',
},
{
name: 'Topics',
value: 'topics',
description: 'Duration of call topics',
},
{
name: 'Trackers',
value: 'trackers',
description: 'Smart tracker and keyword tracker information for the call',
},
{
name: 'Transcript',
value: 'transcript',
description: 'Information about the participants',
},
],
routing: {
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const contentProperties = [
'pointsOfInterest',
'brief',
'highlights',
'keyPoints',
'outline',
'callOutcome',
'structure',
'trackers',
'topics',
];
const exposedFieldsProperties = ['media', 'parties'];
const collaborationProperties = ['publicComments'];
const properties = this.getNodeParameter('options.properties') as string[];
const contentSelector = { exposedFields: {} } as any;
for (const property of properties) {
if (exposedFieldsProperties.includes(property)) {
contentSelector.exposedFields[property] = true;
} else if (contentProperties.includes(property)) {
contentSelector.exposedFields.content ??= {};
contentSelector.exposedFields.content[property] = true;
} else if (collaborationProperties.includes(property)) {
contentSelector.exposedFields.collaboration ??= {};
contentSelector.exposedFields.collaboration[property] = true;
}
}
requestOptions.body ||= {};
Object.assign(requestOptions.body, { contentSelector });
return requestOptions;
},
],
},
output: {
postReceive: [
async function (
this: IExecuteSingleFunctions,
items: INodeExecutionData[],
_responseData: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
const properties = this.getNodeParameter('options.properties') as string[];
if (properties.includes('transcript')) {
for (const item of items) {
const callTranscripts = await gongApiPaginateRequest.call(
this,
'POST',
'/v2/calls/transcript',
{ filter: { callIds: [(item.json.metaData as IDataObject).id] } },
{},
item.index ?? 0,
'callTranscripts',
);
item.json.transcript = callTranscripts?.length
? callTranscripts[0].transcript
: [];
}
}
return items;
},
],
},
},
},
],
placeholder: 'Add Option',
type: 'collection',
},
];
const getAllFields: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
default: false,
description: 'Whether to return all results or only up to a given limit',
displayOptions: {
show: {
resource: ['call'],
operation: ['getAll'],
},
},
routing: {
send: {
paginate: '={{ $value }}',
},
operations: {
pagination: getCursorPaginatorCalls(),
},
},
type: 'boolean',
},
{
displayName: 'Limit',
name: 'limit',
default: 50,
description: 'Max number of results to return',
displayOptions: {
show: {
resource: ['call'],
operation: ['getAll'],
returnAll: [false],
},
},
routing: {
output: {
postReceive: [
async function (
this: IExecuteSingleFunctions,
items: INodeExecutionData[],
_response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
return extractCalls(items);
},
{
type: 'limit',
properties: {
maxResults: '={{ $value }}',
},
},
],
},
},
type: 'number',
typeOptions: {
minValue: 1,
},
validateType: 'number',
},
{
displayName: 'Filters',
name: 'filters',
default: {},
displayOptions: {
show: {
resource: ['call'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'After',
name: 'fromDateTime',
default: '',
description:
'Returns calls that started on or after the specified date and time. If not provided, list starts with earliest call. For web-conference calls recorded by Gong, the date denotes its scheduled time, otherwise, it denotes its actual start time.',
placeholder: 'e.g. 2018-02-18T02:30:00-07:00 or 2018-02-18T08:00:00Z',
routing: {
send: {
type: 'body',
property: 'filter.fromDateTime',
propertyInDotNotation: true,
value: '={{ new Date($value).toISOString() }}',
},
},
type: 'dateTime',
validateType: 'dateTime',
},
{
displayName: 'Before',
name: 'toDateTime',
default: '',
description:
'Returns calls that started up to but excluding specified date and time. If not provided, list ends with most recent call. For web-conference calls recorded by Gong, the date denotes its scheduled time, otherwise, it denotes its actual start time.',
placeholder: 'e.g. 2018-02-18T02:30:00-07:00 or 2018-02-18T08:00:00Z',
routing: {
send: {
type: 'body',
property: 'filter.toDateTime',
propertyInDotNotation: true,
value: '={{ new Date($value).toISOString() }}',
},
},
type: 'dateTime',
validateType: 'dateTime',
},
{
displayName: 'Workspace ID',
name: 'workspaceId',
default: '',
description: 'Return only the calls belonging to this workspace',
placeholder: 'e.g. 623457276584334',
routing: {
send: {
type: 'body',
property: 'filter.workspaceId',
propertyInDotNotation: true,
value: '={{ $value }}',
},
},
type: 'string',
validateType: 'number',
},
{
displayName: 'Call IDs',
name: 'callIds',
default: '',
description: 'List of calls IDs to be filtered',
hint: 'Comma separated list of IDs, array of strings can be set in expression',
routing: {
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const callIdsParam = this.getNodeParameter('filters.callIds') as
| number
| number[]
| string
| string[];
if (callIdsParam && !isValidNumberIds(callIdsParam)) {
throw new NodeApiError(this.getNode(), {
message: 'Call IDs must be numeric',
description: "Double-check the value in the parameter 'Call IDs' and try again",
});
}
const callIds = Array.isArray(callIdsParam)
? callIdsParam.map((x) => x.toString())
: callIdsParam
.toString()
.split(',')
.map((x) => x.trim());
requestOptions.body ||= {};
(requestOptions.body as IDataObject).filter ||= {};
Object.assign((requestOptions.body as IDataObject).filter as IDataObject, {
callIds,
});
return requestOptions;
},
],
},
},
placeholder: 'e.g. 7782342274025937895',
type: 'string',
},
{
displayName: 'Organizer',
name: 'primaryUserIds',
default: {
mode: 'list',
value: '',
},
description: 'Return only the calls hosted by the specified user',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getUsers',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
placeholder: 'e.g. 7782342274025937895',
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '[0-9]{1,20}',
errorMessage: 'Not a valid Gong User ID',
},
},
],
},
],
routing: {
send: {
type: 'body',
property: 'filter.primaryUserIds',
propertyInDotNotation: true,
value: '={{ [$value] }}',
},
},
type: 'resourceLocator',
},
],
placeholder: 'Add Filter',
type: 'collection',
},
{
displayName: 'Options',
name: 'options',
default: {},
displayOptions: {
show: {
resource: ['call'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Call Data to Include',
name: 'properties',
type: 'multiOptions',
default: [],
description:
'The Call properties to include in the returned results. Choose from a list, or specify IDs using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
options: [
{
name: 'Participants',
value: 'parties',
description: 'Information about the participants of the call',
},
{
name: 'Topics',
value: 'topics',
description: 'Information about the topics of the call',
},
],
routing: {
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const contentProperties = ['topics'];
const exposedFieldsProperties = ['parties'];
const properties = this.getNodeParameter('options.properties') as string[];
const contentSelector = { exposedFields: {} } as any;
for (const property of properties) {
if (exposedFieldsProperties.includes(property)) {
contentSelector.exposedFields[property] = true;
} else if (contentProperties.includes(property)) {
contentSelector.exposedFields.content ??= {};
contentSelector.exposedFields.content[property] = true;
}
}
requestOptions.body ||= {};
Object.assign(requestOptions.body, { contentSelector });
return requestOptions;
},
],
},
},
},
],
placeholder: 'Add Option',
type: 'collection',
},
];
export const callFields: INodeProperties[] = [...getFields, ...getAllFields];

View file

@ -0,0 +1,288 @@
import type {
IDataObject,
IExecuteSingleFunctions,
IHttpRequestOptions,
INodeProperties,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import {
getCursorPaginatorUsers,
isValidNumberIds,
handleErrorPostReceive,
} from '../GenericFunctions';
export const userOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['user'],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Retrieve data for a specific user',
action: 'Get user',
routing: {
request: {
method: 'POST',
url: '/v2/users/extensive',
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [handleErrorPostReceive],
},
},
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve a list of users',
action: 'Get many users',
routing: {
request: {
method: 'POST',
url: '/v2/users/extensive',
body: {
filter: {},
},
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [handleErrorPostReceive],
},
},
},
],
default: 'get',
},
];
const getOperation: INodeProperties[] = [
{
displayName: 'User to Get',
name: 'user',
default: {
mode: 'list',
value: '',
},
displayOptions: {
show: {
resource: ['user'],
operation: ['get'],
},
},
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getUsers',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
placeholder: 'e.g. 7782342274025937895',
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '[0-9]{1,20}',
errorMessage: 'Not a valid Gong User ID',
},
},
],
},
],
routing: {
send: {
type: 'body',
property: 'filter.userIds',
propertyInDotNotation: true,
value: '={{ [$value] }}',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'users',
},
},
],
},
},
type: 'resourceLocator',
},
];
const getAllOperation: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
default: false,
description: 'Whether to return all results or only up to a given limit',
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
},
},
routing: {
send: {
paginate: '={{ $value }}',
},
operations: {
pagination: getCursorPaginatorUsers(),
},
},
type: 'boolean',
validateType: 'boolean',
},
{
displayName: 'Limit',
name: 'limit',
default: 50,
description: 'Max number of results to return',
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
returnAll: [false],
},
},
routing: {
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'users',
},
},
{
type: 'limit',
properties: {
maxResults: '={{ $value }}',
},
},
],
},
},
type: 'number',
typeOptions: {
minValue: 1,
},
validateType: 'number',
},
{
displayName: 'Filters',
name: 'filters',
default: {},
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Created After',
name: 'createdFromDateTime',
default: '',
description:
'An optional user creation time lower limit, if supplied the API will return only the users created at or after this time',
placeholder: 'e.g. 2018-02-18T02:30:00-07:00 or 2018-02-18T08:00:00Z',
routing: {
send: {
type: 'body',
property: 'filter.createdFromDateTime',
propertyInDotNotation: true,
value: '={{ new Date($value).toISOString() }}',
},
},
type: 'dateTime',
validateType: 'dateTime',
},
{
displayName: 'Created Before',
name: 'createdToDateTime',
default: '',
description:
'An optional user creation time upper limit, if supplied the API will return only the users created before this time',
placeholder: 'e.g. 2018-02-18T02:30:00-07:00 or 2018-02-18T08:00:00Z',
routing: {
send: {
type: 'body',
property: 'filter.createdToDateTime',
propertyInDotNotation: true,
value: '={{ new Date($value).toISOString() }}',
},
},
type: 'dateTime',
validateType: 'dateTime',
},
{
displayName: 'User IDs',
name: 'userIds',
default: '',
description: "Set of Gong's unique numeric identifiers for the users (up to 20 digits)",
hint: 'Comma separated list of IDs, array of strings can be set in expression',
routing: {
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const userIdsParam = this.getNodeParameter('filters.userIds') as
| number
| number[]
| string
| string[];
if (userIdsParam && !isValidNumberIds(userIdsParam)) {
throw new NodeApiError(this.getNode(), {
message: 'User IDs must be numeric',
description: "Double-check the value in the parameter 'User IDs' and try again",
});
}
const userIds = Array.isArray(userIdsParam)
? userIdsParam.map((x) => x.toString())
: userIdsParam
.toString()
.split(',')
.map((x) => x.trim());
requestOptions.body ||= {};
(requestOptions.body as IDataObject).filter ||= {};
Object.assign((requestOptions.body as IDataObject).filter as IDataObject, {
userIds,
});
return requestOptions;
},
],
},
},
placeholder: 'e.g. 7782342274025937895',
type: 'string',
},
],
placeholder: 'Add Filter',
type: 'collection',
},
];
export const userFields: INodeProperties[] = [...getOperation, ...getAllOperation];

View file

@ -0,0 +1,2 @@
export * from './CallDescription';
export * from './UserDescription';

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="30.0354 21.6399 25.8853 28.023" xmlns="http://www.w3.org/2000/svg">
<path d="M 55.285 33.621 L 47.685 33.621 C 47.251 33.621 46.92 34.065 47.075 34.479 L 48.905 39.215 C 48.985 39.426 48.821 39.65 48.595 39.639 L 46.258 39.494 C 46.151 39.486 46.048 39.537 45.99 39.628 L 44.17 42.203 C 44.074 42.354 43.873 42.396 43.725 42.296 L 41.037 40.466 C 40.931 40.393 40.791 40.393 40.685 40.466 L 36.963 42.989 C 36.725 43.154 36.404 42.927 36.487 42.647 L 37.521 38.935 C 37.566 38.781 37.485 38.619 37.335 38.563 L 35.36 37.757 C 35.164 37.679 35.102 37.433 35.236 37.271 L 36.983 35.12 C 37.069 35.012 37.073 34.86 36.993 34.748 L 35.525 32.628 C 35.389 32.432 35.517 32.162 35.755 32.142 C 35.757 32.142 35.76 32.142 35.763 32.142 L 38.038 31.966 C 38.204 31.956 38.338 31.811 38.328 31.636 L 38.152 28.451 C 38.143 28.221 38.372 28.058 38.586 28.141 L 41.409 29.309 C 41.533 29.361 41.678 29.329 41.76 29.226 L 43.704 27.055 C 43.858 26.882 44.138 26.926 44.232 27.137 L 45.4 30.147 C 45.555 30.519 46.02 30.653 46.352 30.405 L 50.922 27.003 C 51.439 26.62 51.108 25.793 50.467 25.886 L 47.457 26.29 C 47.315 26.31 47.177 26.228 47.127 26.093 L 45.545 22.03 C 45.375 21.605 44.819 21.504 44.511 21.843 L 41.068 25.576 C 40.977 25.67 40.837 25.699 40.716 25.649 L 36.187 23.736 C 35.782 23.568 35.335 23.856 35.319 24.294 L 35.153 28.968 C 35.147 29.127 35.022 29.256 34.863 29.268 L 30.728 29.536 C 30.249 29.566 29.983 30.103 30.248 30.502 C 30.25 30.504 30.251 30.506 30.252 30.508 L 33.002 34.562 C 33.083 34.679 33.075 34.836 32.982 34.944 L 30.19 38.15 C 29.91 38.468 30.025 38.968 30.417 39.132 L 33.623 40.518 C 33.765 40.576 33.84 40.731 33.799 40.879 L 31.751 48.883 C 31.632 49.349 32.062 49.769 32.525 49.639 C 32.596 49.619 32.663 49.587 32.723 49.544 L 40.416 44.023 C 40.524 43.945 40.67 43.945 40.778 44.023 L 44.284 46.483 C 44.573 46.69 44.966 46.608 45.162 46.318 L 47.355 42.958 C 47.418 42.856 47.537 42.802 47.655 42.823 L 52.897 43.454 C 53.321 43.516 53.745 43.164 53.59 42.772 L 51.408 37.126 C 51.346 36.971 51.408 36.806 51.594 36.712 L 55.574 34.83 C 56.164 34.53 55.957 33.62 55.285 33.62 L 55.285 33.621 Z" fill="#9069E7"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,781 @@
/* eslint-disable n8n-nodes-base/node-param-display-name-miscased */
export const gongApiResponse = {
// https://gong.app.gong.io/settings/api/documentation#post-/v2/calls
postCalls: {
requestId: '4al018gzaztcr8nbukw',
callId: '7782342274025937895',
},
// https://gong.app.gong.io/settings/api/documentation#put-/v2/calls/-id-/media
postCallsMedia: {
requestId: '4al018gzaztcr8nbukw',
callId: '7782342274025937895',
url: 'https://app.gong.io/call?id=7782342274025937895',
},
// https://gong.app.gong.io/settings/api/documentation#post-/v2/calls/extensive
postCallsExtensive: {
requestId: '4al018gzaztcr8nbukw',
records: {
totalRecords: 263,
currentPageSize: 100,
currentPageNumber: 0,
cursor: 'eyJhbGciOiJIUzI1NiJ9.eyJjYWxsSWQiM1M30.6qKwpOcvnuweTZmFRzYdtjs_YwJphJU4QIwWFM',
},
calls: [
{
metaData: {
id: '7782342274025937895',
url: 'https://app.gong.io/call?id=7782342274025937895',
title: 'Example call',
scheduled: 1518863400,
started: 1518863400,
duration: 460,
primaryUserId: '234599484848423',
direction: 'Inbound',
system: 'Outreach',
scope: 'Internal',
media: 'Video',
language: 'eng',
workspaceId: '623457276584334',
sdrDisposition: 'Got the gatekeeper',
clientUniqueId: '7JEHFRGXDDZFEW2FC4U',
customData: 'Conference Call',
purpose: 'Demo Call',
meetingUrl: 'https://zoom.us/j/123',
isPrivate: false,
calendarEventId: 'abcde@google.com',
},
context: [
{
system: 'Salesforce',
objects: [
{
objectType: 'Opportunity',
objectId: '0013601230sV7grAAC',
fields: [
{
name: 'name',
value: 'Gong Inc.',
},
],
timing: 'Now',
},
],
},
],
parties: [
{
id: '56825452554556',
emailAddress: 'test@test.com',
name: 'Test User',
title: 'Enterprise Account Executive',
userId: '234599484848423',
speakerId: '6432345678555530067',
context: [
{
system: 'Salesforce',
objects: [
{
objectType: 'Contact',
objectId: '0013601230sV7grAAC',
fields: [
{
name: 'name',
value: 'Gong Inc.',
},
],
timing: 'Now',
},
],
},
],
affiliation: 'Internal',
phoneNumber: '+1 123-567-8989',
methods: ['Invitee'],
},
],
content: {
structure: [
{
name: 'Meeting Setup',
duration: 67,
},
],
trackers: [
{
id: '56825452554556',
name: 'Competitors',
count: 7,
type: 'KEYWORD',
occurrences: [
{
startTime: 32.56,
speakerId: '234599484848423',
},
],
phrases: [
{
count: 5,
occurrences: [
{
startTime: 32.56,
speakerId: '234599484848423',
},
],
phrase: 'Walmart',
},
],
},
],
topics: [
{
name: 'Objections',
duration: 86,
},
],
pointsOfInterest: {
actionItems: [
{
snippetStartTime: 26,
snippetEndTime: 26,
speakerID: '56825452554556',
snippet:
"And I'll send you an invite with a link that you can use at that time as well.",
},
],
},
brief: 'string',
outline: [
{
section: 'string',
startTime: 0.5,
duration: 0.5,
items: [
{
text: 'string',
startTime: 0.5,
},
],
},
],
highlights: [
{
title: 'string',
items: [
{
text: 'string',
startTimes: [0.5],
},
],
},
],
callOutcome: {
id: 'MEETING_BOOKED',
category: 'Answered',
name: 'Meeting booked',
},
keyPoints: [
{
text: 'string',
},
],
},
interaction: {
speakers: [
{
id: '56825452554556',
userId: '234599484848423',
talkTime: 145,
},
],
interactionStats: [
{
name: 'Interactivity',
value: 56,
},
],
video: [
{
name: 'Browser',
duration: 218,
},
],
questions: {
companyCount: 0,
nonCompanyCount: 0,
},
},
collaboration: {
publicComments: [
{
id: '6843152929075440037',
audioStartTime: 26,
audioEndTime: 26,
commenterUserId: '234599484848423',
comment: 'new comment',
posted: 1518863400,
inReplyTo: '792390015966656336',
duringCall: false,
},
],
},
media: {
audioUrl: 'http://example.com',
videoUrl: 'http://example.com',
},
},
],
},
// https://gong.app.gong.io/settings/api/documentation#post-/v2/calls/transcript
postCallsTranscript: {
requestId: '4al018gzaztcr8nbukw',
records: {
totalRecords: 1,
currentPageSize: 1,
currentPageNumber: 0,
},
callTranscripts: [
{
callId: '7782342274025937895',
transcript: [
{
speakerId: '6432345678555530067',
topic: 'Objections',
sentences: [
{
start: 460230,
end: 462343,
text: 'No wait, I think we should check that out first.',
},
],
},
],
},
],
},
// https://gong.app.gong.io/settings/api/documentation#post-/v2/users/extensive
postUsersExtensive: {
requestId: '4al018gzaztcr8nbukw',
records: {
totalRecords: 263,
currentPageSize: 100,
currentPageNumber: 0,
cursor: 'eyJhbGciOiJIUzI1NiJ9.eyJjYWxsSWQiM1M30.6qKwpOcvnuweTZmFRzYdtjs_YwJphJU4QIwWFM',
},
users: [
{
id: '234599484848423',
emailAddress: 'test@test.com',
created: '2018-02-17T02:30:00-08:00',
active: true,
emailAliases: ['testAlias@test.com'],
trustedEmailAddress: 'test@test.com',
firstName: 'Jon',
lastName: 'Snow',
title: 'Enterprise Account Executive',
phoneNumber: '+1 123-567-8989',
extension: '123',
personalMeetingUrls: ['https://zoom.us/j/123'],
settings: {
webConferencesRecorded: true,
preventWebConferenceRecording: false,
telephonyCallsImported: false,
emailsImported: true,
preventEmailImport: false,
nonRecordedMeetingsImported: true,
gongConnectEnabled: true,
},
managerId: '563515258458745',
meetingConsentPageUrl:
'https://join.gong.io/my-company/jon.snow?tkn=MoNpS9tMNt8BK7EZxQpSJl',
spokenLanguages: [
{
language: 'es-ES',
primary: true,
},
],
},
],
},
};
export const gongNodeResponse = {
getCall: [
{
json: {
metaData: {
id: '7782342274025937895',
url: 'https://app.gong.io/call?id=7782342274025937895',
title: 'Example call',
scheduled: 1518863400,
started: 1518863400,
duration: 460,
primaryUserId: '234599484848423',
direction: 'Inbound',
system: 'Outreach',
scope: 'Internal',
media: 'Video',
language: 'eng',
workspaceId: '623457276584334',
sdrDisposition: 'Got the gatekeeper',
clientUniqueId: '7JEHFRGXDDZFEW2FC4U',
customData: 'Conference Call',
purpose: 'Demo Call',
meetingUrl: 'https://zoom.us/j/123',
isPrivate: false,
calendarEventId: 'abcde@google.com',
},
context: [
{
system: 'Salesforce',
objects: [
{
objectType: 'Opportunity',
objectId: '0013601230sV7grAAC',
fields: [
{
name: 'name',
value: 'Gong Inc.',
},
],
timing: 'Now',
},
],
},
],
parties: [
{
id: '56825452554556',
emailAddress: 'test@test.com',
name: 'Test User',
title: 'Enterprise Account Executive',
userId: '234599484848423',
speakerId: '6432345678555530067',
context: [
{
system: 'Salesforce',
objects: [
{
objectType: 'Contact',
objectId: '0013601230sV7grAAC',
fields: [
{
name: 'name',
value: 'Gong Inc.',
},
],
timing: 'Now',
},
],
},
],
affiliation: 'Internal',
phoneNumber: '+1 123-567-8989',
methods: ['Invitee'],
},
],
content: {
structure: [
{
name: 'Meeting Setup',
duration: 67,
},
],
trackers: [
{
id: '56825452554556',
name: 'Competitors',
count: 7,
type: 'KEYWORD',
occurrences: [
{
startTime: 32.56,
speakerId: '234599484848423',
},
],
phrases: [
{
count: 5,
occurrences: [
{
startTime: 32.56,
speakerId: '234599484848423',
},
],
phrase: 'Walmart',
},
],
},
],
topics: [
{
name: 'Objections',
duration: 86,
},
],
pointsOfInterest: {
actionItems: [
{
snippetStartTime: 26,
snippetEndTime: 26,
speakerID: '56825452554556',
snippet:
"And I'll send you an invite with a link that you can use at that time as well.",
},
],
},
brief: 'string',
outline: [
{
section: 'string',
startTime: 0.5,
duration: 0.5,
items: [
{
text: 'string',
startTime: 0.5,
},
],
},
],
highlights: [
{
title: 'string',
items: [
{
text: 'string',
startTimes: [0.5],
},
],
},
],
callOutcome: {
id: 'MEETING_BOOKED',
category: 'Answered',
name: 'Meeting booked',
},
keyPoints: [
{
text: 'string',
},
],
},
interaction: {
speakers: [
{
id: '56825452554556',
userId: '234599484848423',
talkTime: 145,
},
],
interactionStats: [
{
name: 'Interactivity',
value: 56,
},
],
video: [
{
name: 'Browser',
duration: 218,
},
],
questions: {
companyCount: 0,
nonCompanyCount: 0,
},
},
collaboration: {
publicComments: [
{
id: '6843152929075440037',
audioStartTime: 26,
audioEndTime: 26,
commenterUserId: '234599484848423',
comment: 'new comment',
posted: 1518863400,
inReplyTo: '792390015966656336',
duringCall: false,
},
],
},
media: {
audioUrl: 'http://example.com',
videoUrl: 'http://example.com',
},
transcript: [
{
speakerId: '6432345678555530067',
topic: 'Objections',
sentences: [
{
start: 460230,
end: 462343,
text: 'No wait, I think we should check that out first.',
},
],
},
],
},
},
],
getAllCall: [
{
json: {
id: '7782342274025937895',
url: 'https://app.gong.io/call?id=7782342274025937895',
title: 'Example call',
scheduled: 1518863400,
started: 1518863400,
duration: 460,
primaryUserId: '234599484848423',
direction: 'Inbound',
system: 'Outreach',
scope: 'Internal',
media: 'Video',
language: 'eng',
workspaceId: '623457276584334',
sdrDisposition: 'Got the gatekeeper',
clientUniqueId: '7JEHFRGXDDZFEW2FC4U',
customData: 'Conference Call',
purpose: 'Demo Call',
meetingUrl: 'https://zoom.us/j/123',
isPrivate: false,
calendarEventId: 'abcde@google.com',
content: {
topics: [
{
name: 'Objections',
duration: 86,
},
],
},
parties: [
{
id: '56825452554556',
emailAddress: 'test@test.com',
name: 'Test User',
title: 'Enterprise Account Executive',
userId: '234599484848423',
speakerId: '6432345678555530067',
context: [
{
system: 'Salesforce',
objects: [
{
objectType: 'Contact',
objectId: '0013601230sV7grAAC',
fields: [
{
name: 'name',
value: 'Gong Inc.',
},
],
timing: 'Now',
},
],
},
],
affiliation: 'Internal',
phoneNumber: '+1 123-567-8989',
methods: ['Invitee'],
},
],
},
},
{
json: {
id: '7782342274025937896',
url: 'https://app.gong.io/call?id=7782342274025937896',
title: 'Example call',
scheduled: 1518863400,
started: 1518863400,
duration: 460,
primaryUserId: '234599484848423',
direction: 'Inbound',
system: 'Outreach',
scope: 'Internal',
media: 'Video',
language: 'eng',
workspaceId: '623457276584334',
sdrDisposition: 'Got the gatekeeper',
clientUniqueId: '7JEHFRGXDDZFEW2FC4U',
customData: 'Conference Call',
purpose: 'Demo Call',
meetingUrl: 'https://zoom.us/j/123',
isPrivate: false,
calendarEventId: 'abcde@google.com',
content: {
topics: [
{
name: 'Objections',
duration: 86,
},
],
},
parties: [
{
id: '56825452554556',
emailAddress: 'test@test.com',
name: 'Test User',
title: 'Enterprise Account Executive',
userId: '234599484848423',
speakerId: '6432345678555530067',
context: [
{
system: 'Salesforce',
objects: [
{
objectType: 'Contact',
objectId: '0013601230sV7grAAC',
fields: [
{
name: 'name',
value: 'Gong Inc.',
},
],
timing: 'Now',
},
],
},
],
affiliation: 'Internal',
phoneNumber: '+1 123-567-8989',
methods: ['Invitee'],
},
],
},
},
],
getAllCallNoOptions: [
{
json: {
id: '7782342274025937895',
url: 'https://app.gong.io/call?id=7782342274025937895',
title: 'Example call',
scheduled: 1518863400,
started: 1518863400,
duration: 460,
primaryUserId: '234599484848423',
direction: 'Inbound',
system: 'Outreach',
scope: 'Internal',
media: 'Video',
language: 'eng',
workspaceId: '623457276584334',
sdrDisposition: 'Got the gatekeeper',
clientUniqueId: '7JEHFRGXDDZFEW2FC4U',
customData: 'Conference Call',
purpose: 'Demo Call',
meetingUrl: 'https://zoom.us/j/123',
isPrivate: false,
calendarEventId: 'abcde@google.com',
},
},
],
getUser: [
{
json: {
id: '234599484848423',
emailAddress: 'test@test.com',
created: '2018-02-17T02:30:00-08:00',
active: true,
emailAliases: ['testAlias@test.com'],
trustedEmailAddress: 'test@test.com',
firstName: 'Jon',
lastName: 'Snow',
title: 'Enterprise Account Executive',
phoneNumber: '+1 123-567-8989',
extension: '123',
personalMeetingUrls: ['https://zoom.us/j/123'],
settings: {
webConferencesRecorded: true,
preventWebConferenceRecording: false,
telephonyCallsImported: false,
emailsImported: true,
preventEmailImport: false,
nonRecordedMeetingsImported: true,
gongConnectEnabled: true,
},
managerId: '563515258458745',
meetingConsentPageUrl:
'https://join.gong.io/my-company/jon.snow?tkn=MoNpS9tMNt8BK7EZxQpSJl',
spokenLanguages: [
{
language: 'es-ES',
primary: true,
},
],
},
},
],
getAllUser: [
{
json: {
id: '234599484848423',
emailAddress: 'test@test.com',
created: '2018-02-17T02:30:00-08:00',
active: true,
emailAliases: ['testAlias@test.com'],
trustedEmailAddress: 'test@test.com',
firstName: 'Jon',
lastName: 'Snow',
title: 'Enterprise Account Executive',
phoneNumber: '+1 123-567-8989',
extension: '123',
personalMeetingUrls: ['https://zoom.us/j/123'],
settings: {
webConferencesRecorded: true,
preventWebConferenceRecording: false,
telephonyCallsImported: false,
emailsImported: true,
preventEmailImport: false,
nonRecordedMeetingsImported: true,
gongConnectEnabled: true,
},
managerId: '563515258458745',
meetingConsentPageUrl:
'https://join.gong.io/my-company/jon.snow?tkn=MoNpS9tMNt8BK7EZxQpSJl',
spokenLanguages: [
{
language: 'es-ES',
primary: true,
},
],
},
},
{
json: {
id: '234599484848424',
emailAddress: 'test@test.com',
created: '2018-02-17T02:30:00-08:00',
active: true,
emailAliases: ['testAlias@test.com'],
trustedEmailAddress: 'test@test.com',
firstName: 'Jon',
lastName: 'Snow',
title: 'Enterprise Account Executive',
phoneNumber: '+1 123-567-8989',
extension: '123',
personalMeetingUrls: ['https://zoom.us/j/123'],
settings: {
webConferencesRecorded: true,
preventWebConferenceRecording: false,
telephonyCallsImported: false,
emailsImported: true,
preventEmailImport: false,
nonRecordedMeetingsImported: true,
gongConnectEnabled: true,
},
managerId: '563515258458745',
meetingConsentPageUrl:
'https://join.gong.io/my-company/jon.snow?tkn=MoNpS9tMNt8BK7EZxQpSJl',
spokenLanguages: [
{
language: 'es-ES',
primary: true,
},
],
},
},
],
};

View file

@ -125,6 +125,8 @@
"dist/credentials/GitlabOAuth2Api.credentials.js",
"dist/credentials/GitPassword.credentials.js",
"dist/credentials/GmailOAuth2Api.credentials.js",
"dist/credentials/GongApi.credentials.js",
"dist/credentials/GongOAuth2Api.credentials.js",
"dist/credentials/GoogleAdsOAuth2Api.credentials.js",
"dist/credentials/GoogleAnalyticsOAuth2Api.credentials.js",
"dist/credentials/GoogleApi.credentials.js",
@ -517,6 +519,7 @@
"dist/nodes/Github/GithubTrigger.node.js",
"dist/nodes/Gitlab/Gitlab.node.js",
"dist/nodes/Gitlab/GitlabTrigger.node.js",
"dist/nodes/Gong/Gong.node.js",
"dist/nodes/Google/Ads/GoogleAds.node.js",
"dist/nodes/Google/Analytics/GoogleAnalytics.node.js",
"dist/nodes/Google/BigQuery/GoogleBigQuery.node.js",

View file

@ -54,6 +54,32 @@ BQIDAQAB
airtableApi: {
apiKey: 'key123',
},
gongApi: {
baseUrl: 'https://api.gong.io',
accessKey: 'accessKey123',
accessKeySecret: 'accessKeySecret123',
},
gongOAuth2Api: {
grantType: 'authorizationCode',
authUrl: 'https://app.gong.io/oauth2/authorize',
accessTokenUrl: 'https://app.gong.io/oauth2/generate-customer-token',
clientId: 'CLIENTID',
clientSecret: 'CLIENTSECRET',
scope:
'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',
authQueryParameters: '',
authentication: 'header',
oauthTokenData: {
access_token: 'ACCESSTOKEN',
refresh_token: 'REFRESHTOKEN',
scope:
'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',
token_type: 'bearer',
expires_in: 86400,
api_base_url_for_customer: 'https://api.gong.io',
},
baseUrl: 'https://api.gong.io',
},
n8nApi: {
apiKey: 'key123',
baseUrl: 'https://test.app.n8n.cloud/api/v1',

View file

@ -94,7 +94,7 @@ class CredentialType implements ICredentialTypes {
const credentialTypes = new CredentialType();
class CredentialsHelper extends ICredentialsHelper {
export class CredentialsHelper extends ICredentialsHelper {
getCredentialsProperties() {
return [];
}
@ -167,6 +167,8 @@ export function WorkflowExecuteAdditionalData(
return mock<IWorkflowExecuteAdditionalData>({
credentialsHelper: new CredentialsHelper(),
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', mock()),
// Get from node.parameters
currentNodeParameters: undefined,
});
}

View file

@ -2377,7 +2377,7 @@ export interface WorkflowTestData {
nock?: {
baseUrl: string;
mocks: Array<{
method: 'get' | 'post';
method: 'delete' | 'get' | 'post' | 'put';
path: string;
requestBody?: RequestBodyMatcher;
statusCode: number;

View file

@ -41,6 +41,7 @@ import type {
PostReceiveAction,
JsonObject,
CloseFunction,
INodeCredentialDescription,
} from './Interfaces';
import * as NodeHelpers from './NodeHelpers';
import { sleep } from './utils';
@ -88,11 +89,6 @@ export class RoutingNode {
const items = inputData.main[0] as INodeExecutionData[];
const returnData: INodeExecutionData[] = [];
let credentialType: string | undefined;
if (nodeType.description.credentials?.length) {
credentialType = nodeType.description.credentials[0].name;
}
const closeFunctions: CloseFunction[] = [];
const executeFunctions = nodeExecuteFunctions.getExecuteFunctions(
this.workflow,
@ -108,24 +104,45 @@ export class RoutingNode {
abortSignal,
);
let credentialDescription: INodeCredentialDescription | undefined;
if (nodeType.description.credentials?.length) {
if (nodeType.description.credentials.length === 1) {
credentialDescription = nodeType.description.credentials[0];
} else {
const authenticationMethod = executeFunctions.getNodeParameter(
'authentication',
0,
) as string;
credentialDescription = nodeType.description.credentials.find((x) =>
x.displayOptions?.show?.authentication?.includes(authenticationMethod),
);
if (!credentialDescription) {
throw new NodeOperationError(
this.node,
`Node type "${this.node.type}" does not have any credentials of type "${authenticationMethod}" defined`,
{ level: 'warning' },
);
}
}
}
let credentials: ICredentialDataDecryptedObject | undefined;
if (credentialsDecrypted) {
credentials = credentialsDecrypted.data;
} else if (credentialType) {
} else if (credentialDescription) {
try {
credentials =
(await executeFunctions.getCredentials<ICredentialDataDecryptedObject>(credentialType)) ||
{};
(await executeFunctions.getCredentials<ICredentialDataDecryptedObject>(
credentialDescription.name,
)) || {};
} catch (error) {
if (
nodeType.description.credentials?.length &&
nodeType.description.credentials[0].required
) {
if (credentialDescription.required) {
// Only throw error if credential is mandatory
throw error;
} else {
// Do not request cred type since it doesn't exist
credentialType = undefined;
credentialDescription = undefined;
}
}
}
@ -282,7 +299,7 @@ export class RoutingNode {
itemContext[itemIndex].thisArgs,
itemIndex,
runIndex,
credentialType,
credentialDescription?.name,
itemContext[itemIndex].requestData.requestOperations,
credentialsDecrypted,
),