mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-12 15:44:06 -08:00
feat(Gong Node): New node (#10777)
This commit is contained in:
parent
aa3c0dd226
commit
785b47feb3
58
packages/nodes-base/credentials/GongApi.credentials.ts
Normal file
58
packages/nodes-base/credentials/GongApi.credentials.ts
Normal 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',
|
||||
},
|
||||
};
|
||||
}
|
59
packages/nodes-base/credentials/GongOAuth2Api.credentials.ts
Normal file
59
packages/nodes-base/credentials/GongOAuth2Api.credentials.ts
Normal 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',
|
||||
},
|
||||
];
|
||||
}
|
227
packages/nodes-base/nodes/Gong/GenericFunctions.ts
Normal file
227
packages/nodes-base/nodes/Gong/GenericFunctions.ts
Normal 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;
|
||||
}
|
18
packages/nodes-base/nodes/Gong/Gong.node.json
Normal file
18
packages/nodes-base/nodes/Gong/Gong.node.json
Normal 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/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
171
packages/nodes-base/nodes/Gong/Gong.node.ts
Normal file
171
packages/nodes-base/nodes/Gong/Gong.node.ts
Normal 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 };
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
603
packages/nodes-base/nodes/Gong/descriptions/CallDescription.ts
Normal file
603
packages/nodes-base/nodes/Gong/descriptions/CallDescription.ts
Normal 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];
|
288
packages/nodes-base/nodes/Gong/descriptions/UserDescription.ts
Normal file
288
packages/nodes-base/nodes/Gong/descriptions/UserDescription.ts
Normal 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];
|
2
packages/nodes-base/nodes/Gong/descriptions/index.ts
Normal file
2
packages/nodes-base/nodes/Gong/descriptions/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './CallDescription';
|
||||
export * from './UserDescription';
|
4
packages/nodes-base/nodes/Gong/gong.svg
Normal file
4
packages/nodes-base/nodes/Gong/gong.svg
Normal 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 |
1079
packages/nodes-base/nodes/Gong/test/Gong.node.test.ts
Normal file
1079
packages/nodes-base/nodes/Gong/test/Gong.node.test.ts
Normal file
File diff suppressed because it is too large
Load diff
781
packages/nodes-base/nodes/Gong/test/mocks.ts
Normal file
781
packages/nodes-base/nodes/Gong/test/mocks.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
Loading…
Reference in a new issue