Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gainsight NXT new components #14453

Merged
merged 12 commits into from
Nov 6, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { parseObjectEntries } from "../../common/utils.mjs";
import app from "../../gainsight_nxt.app.mjs";

export default {
key: "gainsight_nxt-create-or-update-company",
name: "Create or Update Company",
description: "Create or update a company record. [See the documentation](https://support.gainsight.com/gainsight_nxt/API_and_Developer_Docs/Company_and_Relationship_API/Company_API_Documentation#Parameters)",
version: "0.0.1",
type: "action",
props: {
app,
name: {
type: "string",
label: "Name",
description: "The name of the company. If a company record with this name exists, it will be updated, otherwise a new one will be created.",
},
industry: {
type: "string",
label: "Industry",
description: "The industry name of the company.",
optional: true,
},
arr: {
type: "string",
label: "Annual Recurring Revenue (ARR)",
description: "The annual recurring revenue of the company, as a currency value.",
optional: true,
},
employees: {
type: "integer",
label: "Employees",
description: "The number of employees the company has.",
optional: true,
},
lifecycleInWeeks: {
type: "integer",
label: "Life Cycle in Weeks",
description: "The number of weeks the entire process goes through.",
optional: true,
},
originalContractDate: {
type: "string",
label: "Original Contract Date",
description: "The date the engagement with the client started, in `YYYY-MM-DD` format.",
optional: true,
},
renewalDate: {
type: "string",
label: "Renewal Date",
description: "The upcoming renewal date of the contract, in `YYYY-MM-DD` format.",
optional: true,
},
stage: {
type: "string",
label: "Stage",
description: "The current stage of the company in the sales pipeline.",
optional: true,
options: [
"New Customer",
"Kicked Off",
"Launched",
"Adopting",
"Will Churn",
"Churn",
],
},
status: {
type: "string",
label: "Status",
description: "The current status of the company.",
optional: true,
options: [
"Active",
"Inactive",
"Churn",
],
},
additionalOptions: {
type: "object",
label: "Additional Options",
description:
"Additional parameters to send in the request. [See the documentation](https://support.gainsight.com/gainsight_nxt/API_and_Developer_Docs/Company_and_Relationship_API/Company_API_Documentation#Parameters) for available parameters. Values will be parsed as JSON where applicable.",
optional: true,
},
},
async run({ $ }) {
const data = {
records: [
{
Name: this.name,
Industry: this.industry,
ARR: this.arr,
Employees: this.employees,
LifecycleInWeeks: this.lifecycleInWeeks,
OriginalContractDate: this.originalContractDate,
RenewalDate: this.renewalDate,
Stage: this.stage,
Status: this.status,
...(this.additionalOptions && parseObjectEntries(this.additionalOptions)),
},
],
};

let summary = "";
let result;
try {
const updateReq = await this.app.updateCompany({
$,
data,
});
result = updateReq;
summary = updateReq.result === true
? `Successfully updated company '${this.name}'`
: `Error updating company '${this.name}'`;
}
catch (err) {
const createReq = await this.app.createCompany({
$,
data,
});
result = createReq;
summary = createReq.result === true
? `Successfully created company '${this.name}'`
: `Error creating company '${this.name}'`;
}

$.export(
"$summary",
summary,
);
return result;
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import app from "../../gainsight_nxt.app.mjs";

export default {
key: "gainsight_nxt-create-or-update-custom-object",
name: "Create or Update Custom Object",
description: "Create or update a custom object record. [See the documentation](https://support.gainsight.com/gainsight_nxt/API_and_Developer_Docs/Custom_Object_API/Gainsight_Custom_Object_API_Documentation#Insert_API)",
version: "0.0.1",
type: "action",
props: {
app,
objectName: {
propDefinition: [
app,
"objectName",
],
},
fields: {
type: "string[]",
label: "Key Field(s)",
description: "The field(s) which identify this object (max 3), e.g. `fieldName1`. If a record with the same key field(s) exists, it will be updated, otherwise a new one will be created.",
},
fieldValues: {
type: "object",
label: "Field Values",
description: "The record data to create or update, as key-value pairs.",
},
},
michelle0927 marked this conversation as resolved.
Show resolved Hide resolved
async run({ $ }) {
const { objectName } = this;
const data = {
records: [
this.fieldValues,
],
};

let summary = "";
let result;
try {
result = await this.app.updateCustomObject({
$,
objectName,
data,
params: {
keys: this.fields.join?.() ?? this.fields,
},
});
summary = result.result === true
? `Successfully updated custom object ${objectName}`
: `Error updating custom object ${objectName}`;
}
catch (err) {
result = await this.app.createCustomObject({
$,
objectName,
data,
});
summary = result.result === true
? `Successfully created custom object ${objectName}`
: `Error creating custom object ${objectName}`;
}

$.export(
"$summary",
summary,
);
return result;
},
michelle0927 marked this conversation as resolved.
Show resolved Hide resolved
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import app from "../../gainsight_nxt.app.mjs";

export default {
key: "gainsight_nxt-create-or-update-person",
name: "Create or Update Person",
description: "Create or update a person's record. [See the documentation](https://support.gainsight.com/gainsight_nxt/API_and_Developer_Docs/Person_API/People_API_Documentation#Person)",
version: "0.0.1",
type: "action",
props: {
app,
email: {
type: "string",
label: "Email",
description: "The email address of the person. If a record with this email exists, it will be updated, otherwise a new one will be created.",
},
firstName: {
type: "string",
label: "First Name",
description: "The first name of the person.",
optional: true,
},
lastName: {
type: "string",
label: "Last Name",
description: "The last name of the person.",
optional: true,
},
linkedinUrl: {
type: "string",
label: "LinkedIn URL",
description: "The LinkedIn URL of the person.",
optional: true,
},
location: {
type: "string",
label: "Location",
description: "The location of the person.",
optional: true,
},
additionalFields: {
type: "object",
label: "Additional Fields",
description: "Additional fields to include in the request body. [See the documentation](https://support.gainsight.com/gainsight_nxt/API_and_Developer_Docs/Person_API/People_API_Documentation#Person) for all available fields.",
optional: true,
},
},
michelle0927 marked this conversation as resolved.
Show resolved Hide resolved
async run({ $ }) {
const response = await this.app.createOrUpdatePerson({
$,
data: {
Email: this.email,
FirstName: this.firstName,
LastName: this.lastName,
LinkedinUrl: this.linkedinUrl,
Location: this.location,
...this.additionalFields,
},
});

$.export("$summary", `Successfully upserted person with email ${this.email}`);
return response;
},
};
michelle0927 marked this conversation as resolved.
Show resolved Hide resolved
22 changes: 22 additions & 0 deletions components/gainsight_nxt/common/utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
function optionalParseAsJSON(value) {
try {
return JSON.parse(value);
} catch (e) {
return value;
}
}
Comment on lines +1 to +7
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider enhancing error handling and type safety.

While the function provides basic JSON parsing with fallback, consider these improvements for better robustness:

  1. Add input validation
  2. Handle undefined/null values
  3. Add error logging for debugging
-function optionalParseAsJSON(value) {
+function optionalParseAsJSON(value) {
+  if (value == null) return value;
+  if (typeof value !== 'string') return value;
   try {
     return JSON.parse(value);
   } catch (e) {
+    console.debug(`Failed to parse JSON: ${e.message}`);
     return value;
   }
 }
📝 Committable suggestion

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

Suggested change
function optionalParseAsJSON(value) {
try {
return JSON.parse(value);
} catch (e) {
return value;
}
}
function optionalParseAsJSON(value) {
if (value == null) return value;
if (typeof value !== 'string') return value;
try {
return JSON.parse(value);
} catch (e) {
console.debug(`Failed to parse JSON: ${e.message}`);
return value;
}
}


export function parseObjectEntries(value) {
const obj = typeof value === "string"
? JSON.parse(value)
: value;
return Object.fromEntries(
Object.entries(obj).map(([
key,
value,
]) => [
key,
optionalParseAsJSON(value),
]),
);
}
Comment on lines +9 to +22
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add defensive programming measures.

The function needs better error handling and input validation to prevent runtime errors:

  1. Direct JSON.parse call could throw on invalid JSON
  2. No null/undefined checks before accessing Object methods
  3. No validation of object structure

Apply these improvements:

 export function parseObjectEntries(value) {
+  if (value == null) {
+    throw new Error('Input value cannot be null or undefined');
+  }
+
   const obj = typeof value === "string"
-    ? JSON.parse(value)
+    ? optionalParseAsJSON(value)
     : value;
+
+  if (typeof obj !== 'object' || Array.isArray(obj)) {
+    throw new Error('Input must be an object or valid JSON object string');
+  }
+
   return Object.fromEntries(
     Object.entries(obj).map(([
       key,
       value,
     ]) => [
       key,
       optionalParseAsJSON(value),
     ]),
   );
 }
📝 Committable suggestion

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

Suggested change
export function parseObjectEntries(value) {
const obj = typeof value === "string"
? JSON.parse(value)
: value;
return Object.fromEntries(
Object.entries(obj).map(([
key,
value,
]) => [
key,
optionalParseAsJSON(value),
]),
);
}
export function parseObjectEntries(value) {
if (value == null) {
throw new Error('Input value cannot be null or undefined');
}
const obj = typeof value === "string"
? optionalParseAsJSON(value)
: value;
if (typeof obj !== 'object' || Array.isArray(obj)) {
throw new Error('Input must be an object or valid JSON object string');
}
return Object.fromEntries(
Object.entries(obj).map(([
key,
value,
]) => [
key,
optionalParseAsJSON(value),
]),
);
}

89 changes: 85 additions & 4 deletions components/gainsight_nxt/gainsight_nxt.app.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,92 @@
import { axios } from "@pipedream/platform";

export default {
type: "app",
app: "gainsight_nxt",
propDefinitions: {},
propDefinitions: {
objectName: {
type: "string",
label: "Custom Object",
description: "The name of the custom object.",
async options() {
const { data } = await this.listCustomObjects();
return data?.filter?.((obj) => obj.objectType === "CUSTOM").map(( {
label, objectName,
}) => ({
label,
value: objectName,
}));
Comment on lines +13 to +18
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Based on the code analysis, I can now provide a final response:

Remove the objectType filter to support all valid object types

The current implementation filters objects by objectType === "CUSTOM", but the API endpoints show that the integration handles various object types including built-in ones like "Company". The filter should be removed as it may exclude valid objects that can be operated on through the /data/objects/{objectName} endpoints.

  • Remove the filter at line 13: data?.filter?.((obj) => obj.objectType === "CUSTOM")
  • Keep only the mapping part to return all available objects: data?.map(({label, objectName}) => ({label, value: objectName}))
🔗 Analysis chain

Reconsider the custom object type filter

Based on the previous discussion, several custom object types may not be explicitly classified as "CUSTOM", which could result in missing valid objects. Consider either:

  1. Removing the filter to show all available objects, or
  2. Validating the complete list of object types to ensure no valid objects are excluded

Let's verify the object types:


Let me search for more specific information about Gainsight NXT's object types.


Let me check the API implementation and any test files that might reveal the expected object types.


Let me fix the regex and try again.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check the available object types in the API response
rg -A 5 "objectType.*=.*" .

Length of output: 17301


Script:

#!/bin/bash
# Search for any references to object types or API responses in Gainsight NXT files
rg -A 5 "objectType|getObjects|listObjects" components/gainsight_nxt/

Length of output: 534


Script:

#!/bin/bash
# Search for API calls and test files related to object types in Gainsight NXT
rg -g "*.{mjs,js,test.js}" -B 3 -A 5 "\/objects|\/custom-objects|objectTypes" components/gainsight_nxt/

Length of output: 210


Script:

#!/bin/bash
# Search for API calls and test files related to object types in Gainsight NXT
rg -g "*.{mjs,js,test.js}" -B 3 -A 5 "(objects|custom-objects|objectTypes)" components/gainsight_nxt/

# Also search for any API-related code that might show object type handling
ast-grep --pattern 'async $FUNC($$$) {
  $$$
  const $_ = await this.$_.$_($$$)
  $$$
}' components/gainsight_nxt/

Length of output: 3201

},
},
},
methods: {
// this.$auth contains connected account data
authKeys() {
console.log(Object.keys(this.$auth));
_baseUrl() {
return `${this.$auth.customer_domain}/v1`;
},
GTFalcao marked this conversation as resolved.
Show resolved Hide resolved
async _makeRequest({
$ = this,
path,
headers = {},
...otherOpts
} = {}) {
return axios($, {
...otherOpts,
url: this._baseUrl() + path,
headers: {
"content-type": "application/json",
"accept": "application/json, text/plain, */*",
"accept-language": "en-GB,en-US;q=0.9,en;q=0.8",
"accesskey": `${this.$auth.access_key}`,
...headers,
},
});
michelle0927 marked this conversation as resolved.
Show resolved Hide resolved
},
Comment on lines +26 to +43
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add path parameter validation in _makeRequest

The path parameter is directly concatenated with the base URL, which could lead to path traversal issues. Consider adding validation to ensure the path:

  1. Starts with a forward slash
  2. Contains no relative path segments (../)
  3. Contains only allowed characters

Add path validation:

     async _makeRequest({
       $ = this,
       path,
       headers = {},
       ...otherOpts
     } = {}) {
+      if (!path || typeof path !== 'string' || !path.startsWith('/') || 
+          path.includes('../') || !/^[\w\-./]+$/.test(path)) {
+        throw new Error('Invalid path parameter');
+      }
       return axios($, {
         ...otherOpts,
         url: this._baseUrl() + path,
📝 Committable suggestion

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

Suggested change
async _makeRequest({
$ = this,
path,
headers = {},
...otherOpts
} = {}) {
return axios($, {
...otherOpts,
url: this._baseUrl() + path,
headers: {
"content-type": "application/json",
"accept": "application/json, text/plain, */*",
"accept-language": "en-GB,en-US;q=0.9,en;q=0.8",
"accesskey": `${this.$auth.access_key}`,
...headers,
},
});
},
async _makeRequest({
$ = this,
path,
headers = {},
...otherOpts
} = {}) {
if (!path || typeof path !== 'string' || !path.startsWith('/') ||
path.includes('../') || !/^[\w\-./]+$/.test(path)) {
throw new Error('Invalid path parameter');
}
return axios($, {
...otherOpts,
url: this._baseUrl() + path,
headers: {
"content-type": "application/json",
"accept": "application/json, text/plain, */*",
"accept-language": "en-GB,en-US;q=0.9,en;q=0.8",
"accesskey": `${this.$auth.access_key}`,
...headers,
},
});
},

async updateCompany(args) {
return this._makeRequest({
path: "/data/objects/Company",
method: "PUT",
params: {
keys: "Name",
},
...args,
});
},
async createCompany(args) {
return this._makeRequest({
path: "/data/objects/Company",
method: "POST",
...args,
});
},
Comment on lines +44 to +60
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add retry logic for company operations

The company creation/update methods could benefit from retry logic with exponential backoff for transient failures.

Consider implementing a retry wrapper:

+    async _withRetry(operation, maxRetries = 3) {
+      for (let i = 0; i < maxRetries; i++) {
+        try {
+          return await operation();
+        } catch (error) {
+          if (i === maxRetries - 1 || !this._isRetryable(error)) throw error;
+          await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
+        }
+      }
+    },
+
     async updateCompany(args) {
-      return this._makeRequest({
+      return this._withRetry(() => this._makeRequest({
         path: "/data/objects/Company",
         method: "PUT",
         params: {
           keys: "Name",
         },
         ...args,
-      });
+      }));
     },

Committable suggestion was skipped due to low confidence.

async createOrUpdatePerson(args) {
return this._makeRequest({
path: "/peoplemgmt/v1.0/people",
method: "PUT",
...args,
});
},
async listCustomObjects() {
return this._makeRequest({
path: "/meta/services/objects/list",
});
},
async updateCustomObject({
objectName, ...args
}) {
return this._makeRequest({
path: `/data/objects/${objectName}`,
method: "PUT",
...args,
});
},
async createCustomObject({
objectName, ...args
}) {
return this._makeRequest({
path: `/data/objects/${objectName}`,
method: "POST",
...args,
});
GTFalcao marked this conversation as resolved.
Show resolved Hide resolved
},
},
};
Loading
Loading