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.",
},
},
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;
},
};
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,
},
},
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;
},
};
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.map(( {
label, objectName,
}) => ({
label,
value: objectName,
}));
},
},
},
methods: {
// this.$auth contains connected account data
authKeys() {
console.log(Object.keys(this.$auth));
_baseUrl() {
return `${this.$auth.customer_domain}/v1`;
},
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,
},
});
},
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,
});
},
},
};
Loading
Loading