first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -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;
}
@@ -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/"
}
]
}
}
+172
View File
@@ -0,0 +1,172 @@
import {
NodeConnectionTypes,
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',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.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 };
},
},
};
}
@@ -0,0 +1,125 @@
{
"type": "object",
"properties": {
"metaData": {
"type": "object",
"properties": {
"customData": {
"type": "null"
},
"direction": {
"type": "string"
},
"duration": {
"type": "integer"
},
"id": {
"type": "string"
},
"isPrivate": {
"type": "boolean"
},
"language": {
"type": "string"
},
"media": {
"type": "string"
},
"meetingUrl": {
"type": "string"
},
"primaryUserId": {
"type": "string"
},
"purpose": {
"type": "null"
},
"scheduled": {
"type": "string"
},
"scope": {
"type": "string"
},
"started": {
"type": "string"
},
"system": {
"type": "string"
},
"title": {
"type": "string"
},
"url": {
"type": "string"
},
"workspaceId": {
"type": "string"
}
}
},
"parties": {
"type": "array",
"items": {
"type": "object",
"properties": {
"affiliation": {
"type": "string"
},
"emailAddress": {
"type": "string"
},
"id": {
"type": "string"
},
"methods": {
"type": "array",
"items": {
"type": "string"
}
},
"name": {
"type": "string"
},
"phoneNumber": {
"type": "string"
},
"title": {
"type": "string"
},
"userId": {
"type": "string"
}
}
}
},
"transcript": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sentences": {
"type": "array",
"items": {
"type": "object",
"properties": {
"end": {
"type": "integer"
},
"start": {
"type": "integer"
},
"text": {
"type": "string"
}
}
}
},
"speakerId": {
"type": "string"
}
}
}
}
},
"version": 1
}
@@ -0,0 +1,57 @@
{
"type": "object",
"properties": {
"customData": {
"type": "null"
},
"direction": {
"type": "string"
},
"duration": {
"type": "integer"
},
"id": {
"type": "string"
},
"isPrivate": {
"type": "boolean"
},
"language": {
"type": "string"
},
"media": {
"type": "string"
},
"meetingUrl": {
"type": "string"
},
"primaryUserId": {
"type": "string"
},
"purpose": {
"type": "null"
},
"scheduled": {
"type": "string"
},
"scope": {
"type": "string"
},
"started": {
"type": "string"
},
"system": {
"type": "string"
},
"title": {
"type": "string"
},
"url": {
"type": "string"
},
"workspaceId": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,76 @@
{
"type": "object",
"properties": {
"active": {
"type": "boolean"
},
"created": {
"type": "string"
},
"emailAddress": {
"type": "string"
},
"emailAliases": {
"type": "array",
"items": {
"type": "string"
}
},
"extension": {
"type": "null"
},
"firstName": {
"type": "string"
},
"id": {
"type": "string"
},
"lastName": {
"type": "string"
},
"settings": {
"type": "object",
"properties": {
"emailsImported": {
"type": "boolean"
},
"gongConnectEnabled": {
"type": "boolean"
},
"nonRecordedMeetingsImported": {
"type": "boolean"
},
"preventEmailImport": {
"type": "boolean"
},
"preventWebConferenceRecording": {
"type": "boolean"
},
"telephonyCallsImported": {
"type": "boolean"
},
"webConferencesRecorded": {
"type": "boolean"
}
}
},
"spokenLanguages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"language": {
"type": "string"
},
"primary": {
"type": "boolean"
}
}
}
},
"trustedEmailAddress": {
"type": "null"
}
},
"version": 1
}
@@ -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];
@@ -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];
@@ -0,0 +1,2 @@
export * from './CallDescription';
export * from './UserDescription';
+4
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

@@ -0,0 +1,983 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import type { WorkflowTestData } from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import { gongApiResponse, gongNodeResponse } from './mocks';
describe('Gong Node', () => {
const testHarness = new NodeTestHarness();
const baseUrl = 'https://api.gong.io';
const credentials = {
gongApi: { baseUrl },
gongOAuth2Api: { baseUrl },
};
describe('Credentials', () => {
const tests: WorkflowTestData[] = [
{
description: 'should use correct credentials',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: 'When clicking Execute workflow',
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
operation: 'get',
call: {
__rl: true,
value: '7782342274025937895',
mode: 'id',
},
options: {},
requestOptions: {},
},
id: 'c87d72ec-0683-4e32-9829-5e6ea1d1ee7d',
name: 'Gong gongApi',
type: 'n8n-nodes-base.gong',
typeVersion: 1,
position: [1040, 380],
credentials: {
gongApi: {
id: '1',
name: 'Gong account',
},
},
},
{
parameters: {
authentication: 'oAuth2',
operation: 'get',
call: {
__rl: true,
value: '7782342274025937896',
mode: 'id',
},
options: {},
requestOptions: {},
},
id: 'c87d72ec-0683-4e32-9829-5e6ea1d1ee7d',
name: 'Gong gongOAuth2Api',
type: 'n8n-nodes-base.gong',
typeVersion: 1,
position: [1040, 380],
credentials: {
gongOAuth2Api: {
id: '2',
name: 'Gong account2',
},
},
},
],
connections: {
'When clicking Execute workflow': {
main: [
[
{
node: 'Gong gongApi',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
'Gong gongApi': {
main: [
[
{
node: 'Gong gongOAuth2Api',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeData: {
'Gong gongApi': [[{ json: { metaData: gongNodeResponse.getCall[0].json.metaData } }]],
'Gong gongOAuth2Api': [
[{ json: { metaData: gongNodeResponse.getCall[0].json.metaData } }],
],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'post',
path: '/v2/calls/extensive',
requestBody: { filter: { callIds: ['7782342274025937895'] } },
statusCode: 200,
responseBody: {
...gongApiResponse.postCallsExtensive,
records: {},
calls: [{ metaData: gongApiResponse.postCallsExtensive.calls[0].metaData }],
},
},
{
method: 'post',
path: '/v2/calls/extensive',
requestBody: { filter: { callIds: ['7782342274025937896'] } },
statusCode: 200,
responseBody: {
...gongApiResponse.postCallsExtensive,
records: {},
calls: [{ metaData: gongApiResponse.postCallsExtensive.calls[0].metaData }],
},
},
],
},
},
];
for (const testData of tests) {
testHarness.setupTest(testData, { credentials });
}
});
describe('Call description', () => {
const tests: WorkflowTestData[] = [
{
description: 'should get call with no options true',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: 'When clicking Execute workflow',
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
operation: 'get',
call: {
__rl: true,
value: '7782342274025937895',
mode: 'id',
},
options: {},
requestOptions: {},
},
id: 'c87d72ec-0683-4e32-9829-5e6ea1d1ee7d',
name: 'Gong',
type: 'n8n-nodes-base.gong',
typeVersion: 1,
position: [1040, 380],
credentials: {
gongApi: {
id: '1',
name: 'Gong account',
},
},
},
],
connections: {
'When clicking Execute workflow': {
main: [
[
{
node: 'Gong',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeData: {
Gong: [[{ json: { metaData: gongNodeResponse.getCall[0].json.metaData } }]],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'post',
path: '/v2/calls/extensive',
statusCode: 200,
requestBody: { filter: { callIds: ['7782342274025937895'] } },
responseBody: {
...gongApiResponse.postCallsExtensive,
records: {},
calls: [{ metaData: gongApiResponse.postCallsExtensive.calls[0].metaData }],
},
},
],
},
},
{
description: 'should get call with all options true',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: 'When clicking Execute workflow',
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
operation: 'get',
call: {
__rl: true,
value: '7782342274025937895',
mode: 'id',
},
options: {
properties: [
'pointsOfInterest',
'transcript',
'media',
'brief',
'publicComments',
'highlights',
'trackers',
'topics',
'structure',
'parties',
'callOutcome',
'outline',
'keyPoints',
],
},
requestOptions: {},
},
id: 'c87d72ec-0683-4e32-9829-5e6ea1d1ee7d',
name: 'Gong',
type: 'n8n-nodes-base.gong',
typeVersion: 1,
position: [1040, 380],
credentials: {
gongApi: {
id: '1',
name: 'Gong account',
},
},
},
],
connections: {
'When clicking Execute workflow': {
main: [
[
{
node: 'Gong',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeData: {
Gong: [gongNodeResponse.getCall],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'post',
path: '/v2/calls/extensive',
statusCode: 200,
requestBody: {
filter: {
callIds: ['7782342274025937895'],
},
contentSelector: {
exposedFields: {
content: {
pointsOfInterest: true,
brief: true,
highlights: true,
keyPoints: true,
outline: true,
callOutcome: true,
structure: true,
trackers: true,
topics: true,
},
media: true,
collaboration: {
publicComments: true,
},
parties: true,
},
},
},
responseBody: {
...gongApiResponse.postCallsExtensive,
records: {},
},
},
{
method: 'post',
path: '/v2/calls/transcript',
statusCode: 200,
requestBody: {
filter: {
callIds: ['7782342274025937895'],
},
},
responseBody: gongApiResponse.postCallsTranscript,
},
],
},
},
{
description: 'should get all calls with filters',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: 'When clicking Execute workflow',
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
returnAll: true,
filters: {
fromDateTime: '2024-01-01T00:00:00Z',
toDateTime: '2024-12-31T00:00:00Z',
workspaceId: '3662366901393371750',
callIds: "={{ ['3662366901393371750', '3662366901393371751'] }}",
primaryUserIds: {
__rl: true,
value: '234599484848423',
mode: 'id',
},
},
options: {
properties: ['parties', 'topics'],
},
requestOptions: {},
},
id: 'c87d72ec-0683-4e32-9829-5e6ea1d1ee7d',
name: 'Gong',
type: 'n8n-nodes-base.gong',
typeVersion: 1,
position: [1040, 380],
credentials: {
gongApi: {
id: '1',
name: 'Gong account',
},
},
},
],
connections: {
'When clicking Execute workflow': {
main: [
[
{
node: 'Gong',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeData: {
Gong: [gongNodeResponse.getAllCall],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'post',
path: '/v2/calls/extensive',
statusCode: 200,
requestBody: {
filter: {
fromDateTime: '2024-01-01T00:00:00.000Z',
toDateTime: '2024-12-31T00:00:00.000Z',
workspaceId: '3662366901393371750',
callIds: ['3662366901393371750', '3662366901393371751'],
primaryUserIds: ['234599484848423'],
},
contentSelector: {
exposedFields: {
parties: true,
content: {
topics: true,
},
},
},
cursor: undefined,
},
responseBody: {
...gongApiResponse.postCallsExtensive,
calls: [
{
metaData: {
...gongApiResponse.postCallsExtensive.calls[0].metaData,
parties: [...gongApiResponse.postCallsExtensive.calls[0].parties],
content: {
topics: [...gongApiResponse.postCallsExtensive.calls[0].content.topics],
},
},
},
],
},
},
{
method: 'post',
path: '/v2/calls/extensive',
statusCode: 200,
requestBody: {
filter: {
fromDateTime: '2024-01-01T00:00:00.000Z',
toDateTime: '2024-12-31T00:00:00.000Z',
workspaceId: '3662366901393371750',
callIds: ['3662366901393371750', '3662366901393371751'],
primaryUserIds: ['234599484848423'],
},
contentSelector: {
exposedFields: {
parties: true,
content: {
topics: true,
},
},
},
cursor:
'eyJhbGciOiJIUzI1NiJ9.eyJjYWxsSWQiM1M30.6qKwpOcvnuweTZmFRzYdtjs_YwJphJU4QIwWFM',
},
responseBody: {
...gongApiResponse.postCallsExtensive,
records: {},
calls: [
{
metaData: {
...gongApiResponse.postCallsExtensive.calls[0].metaData,
id: '7782342274025937896',
url: 'https://app.gong.io/call?id=7782342274025937896',
},
parties: [...gongApiResponse.postCallsExtensive.calls[0].parties],
content: {
topics: [...gongApiResponse.postCallsExtensive.calls[0].content.topics],
},
},
],
},
},
],
},
},
{
description: 'should get limit 50 calls with no options and filters',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: 'When clicking Execute workflow',
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
filters: {},
options: {},
requestOptions: {},
},
id: 'c87d72ec-0683-4e32-9829-5e6ea1d1ee7d',
name: 'Gong',
type: 'n8n-nodes-base.gong',
typeVersion: 1,
position: [1040, 380],
credentials: {
gongApi: {
id: '1',
name: 'Gong account',
},
},
},
],
connections: {
'When clicking Execute workflow': {
main: [
[
{
node: 'Gong',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeData: {
Gong: [
Array.from({ length: 50 }, () => ({ ...gongNodeResponse.getAllCallNoOptions[0] })),
],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'post',
path: '/v2/calls/extensive',
statusCode: 200,
requestBody: {
filter: {},
},
responseBody: {
...gongApiResponse.postCallsExtensive,
calls: Array.from({ length: 100 }, () => ({
metaData: { ...gongApiResponse.postCallsExtensive.calls[0].metaData },
})),
},
},
],
},
},
{
description: 'should return empty result if no calls found for user',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: 'When clicking Execute workflow',
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
filters: {
primaryUserIds: {
__rl: true,
value: '234599484848423',
mode: 'id',
},
},
options: {},
requestOptions: {},
},
id: 'c87d72ec-0683-4e32-9829-5e6ea1d1ee7d',
name: 'Gong',
type: 'n8n-nodes-base.gong',
typeVersion: 1,
position: [1040, 380],
credentials: {
gongApi: {
id: '1',
name: 'Gong account',
},
},
},
],
connections: {
'When clicking Execute workflow': {
main: [
[
{
node: 'Gong',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeData: {
Gong: [[{ json: {} }]],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'post',
path: '/v2/calls/extensive',
statusCode: 404,
requestBody: {
filter: {
primaryUserIds: ['234599484848423'],
},
cursor: undefined,
},
responseBody: {
requestId: 'thrhbxbkqiw41ma1cl',
errors: ['No calls found corresponding to the provided filters'],
},
},
],
},
},
{
description: 'should handle error response',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: 'When clicking Execute workflow',
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
filters: {
workspaceId: '623457276584335',
},
options: {},
requestOptions: {},
},
id: 'c87d72ec-0683-4e32-9829-5e6ea1d1ee7d',
name: 'Gong',
type: 'n8n-nodes-base.gong',
typeVersion: 1,
position: [1040, 380],
credentials: {
gongApi: {
id: '1',
name: 'Gong account',
},
},
},
],
connections: {
'When clicking Execute workflow': {
main: [
[
{
node: 'Gong',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeData: {
Gong: [],
},
error: 'The resource you are requesting could not be found',
},
nock: {
baseUrl,
mocks: [
{
method: 'post',
path: '/v2/calls/extensive',
statusCode: 404,
requestBody: {
filter: {
workspaceId: '623457276584335',
},
},
responseBody: {
requestId: 'thrhbxbkqiw41ma1cl',
errors: ['No calls found corresponding to the provided filters'],
},
},
],
},
},
];
for (const testData of tests) {
testHarness.setupTest(testData, { credentials });
}
});
describe('User description', () => {
const tests: WorkflowTestData[] = [
{
description: 'should get user',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: 'When clicking Execute workflow',
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
resource: 'user',
user: {
__rl: true,
value: '234599484848423',
mode: 'id',
},
requestOptions: {},
},
id: 'c87d72ec-0683-4e32-9829-5e6ea1d1ee7d',
name: 'Gong',
type: 'n8n-nodes-base.gong',
typeVersion: 1,
position: [1040, 380],
credentials: {
gongApi: {
id: '1',
name: 'Gong account',
},
},
},
],
connections: {
'When clicking Execute workflow': {
main: [
[
{
node: 'Gong',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeData: {
Gong: [gongNodeResponse.getUser],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'post',
path: '/v2/users/extensive',
statusCode: 200,
requestBody: { filter: { userIds: ['234599484848423'] } },
responseBody: {
...gongApiResponse.postUsersExtensive,
records: {},
},
},
],
},
},
{
description: 'should get all users',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: 'When clicking Execute workflow',
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
resource: 'user',
operation: 'getAll',
returnAll: true,
filters: {
createdFromDateTime: '2024-01-01T00:00:00Z',
createdToDateTime: '2024-12-31T00:00:00Z',
userIds: '234599484848423, 234599484848424',
},
requestOptions: {},
},
id: 'c87d72ec-0683-4e32-9829-5e6ea1d1ee7d',
name: 'Gong',
type: 'n8n-nodes-base.gong',
typeVersion: 1,
position: [1040, 380],
credentials: {
gongApi: {
id: '1',
name: 'Gong account',
},
},
},
],
connections: {
'When clicking Execute workflow': {
main: [
[
{
node: 'Gong',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeData: {
Gong: [gongNodeResponse.getAllUser],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'post',
path: '/v2/users/extensive',
statusCode: 200,
requestBody: {
filter: {
createdFromDateTime: '2024-01-01T00:00:00.000Z',
createdToDateTime: '2024-12-31T00:00:00.000Z',
userIds: ['234599484848423', '234599484848424'],
},
},
responseBody: gongApiResponse.postUsersExtensive,
},
{
method: 'post',
path: '/v2/users/extensive',
statusCode: 200,
requestBody: {
filter: {
createdFromDateTime: '2024-01-01T00:00:00.000Z',
createdToDateTime: '2024-12-31T00:00:00.000Z',
userIds: ['234599484848423', '234599484848424'],
},
cursor:
'eyJhbGciOiJIUzI1NiJ9.eyJjYWxsSWQiM1M30.6qKwpOcvnuweTZmFRzYdtjs_YwJphJU4QIwWFM',
},
responseBody: {
...gongApiResponse.postUsersExtensive,
records: {},
users: [{ ...gongApiResponse.postUsersExtensive.users[0], id: '234599484848424' }],
},
},
],
},
},
{
description: 'should handle error response',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: 'When clicking Execute workflow',
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
resource: 'user',
operation: 'getAll',
filters: {
userIds: '234599484848423',
},
requestOptions: {},
},
id: 'c87d72ec-0683-4e32-9829-5e6ea1d1ee7d',
name: 'Gong',
type: 'n8n-nodes-base.gong',
typeVersion: 1,
position: [1040, 380],
credentials: {
gongApi: {
id: '1',
name: 'Gong account',
},
},
},
],
connections: {
'When clicking Execute workflow': {
main: [
[
{
node: 'Gong',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeData: {
Gong: [],
},
error: "The Users IDs don't match any existing user",
},
nock: {
baseUrl,
mocks: [
{
method: 'post',
path: '/v2/users/extensive',
statusCode: 404,
requestBody: {
filter: {
userIds: ['234599484848423'],
},
},
responseBody: {
requestId: '26r8maav84ehguoddd7',
errors: ['The following userIds were not found: 234599484848423'],
},
},
],
},
},
];
for (const testData of tests) {
testHarness.setupTest(testData, { credentials });
}
});
});
@@ -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,
},
],
},
},
],
};