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,145 @@
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",
],
},
csmFirstName: {
type: "string",
label: "CSM First Name",
description: "The first name of the POC of the company.",
optional: true,
},
csmLastName: {
type: "string",
label: "CSM Last Name",
description: "The last name of the POC of the company.",
optional: true,
},
parentCompanyName: {
type: "string",
label: "Parent Company Name",
description: "The name of the parent company.",
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,
CSMFirstName: this.csmFirstName,
CSMLastName: this.csmLastName,
ParentCompanyName: this.parentCompanyName,
},
],
};

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;
},
michelle0927 marked this conversation as resolved.
Show resolved Hide resolved
};
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
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,
}));
},
},
},
GTFalcao marked this conversation as resolved.
Show resolved Hide resolved
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
},
},
};
7 changes: 5 additions & 2 deletions components/gainsight_nxt/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipedream/gainsight_nxt",
"version": "0.0.1",
"version": "0.1.0",
"description": "Pipedream Gainsight Components",
"main": "gainsight_nxt.app.mjs",
"keywords": [
Expand All @@ -11,5 +11,8 @@
"author": "Pipedream <[email protected]> (https://pipedream.com/)",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@pipedream/platform": "^3.0.3"
}
}
}
Loading
Loading