From 3dcdab81ed5d67df800bdf83c7865e4dbf80d85e Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Thu, 14 Nov 2024 19:21:40 -0300 Subject: [PATCH 1/3] cats init --- .../add-candidate-pipeline.mjs | 45 ++++ .../create-candidate/create-candidate.mjs | 184 ++++++++++++++ .../actions/create-contact/create-contact.mjs | 121 ++++++++++ components/cats/cats.app.mjs | 224 +++++++++++++++++- components/cats/package.json | 2 +- .../new-activity-instant.mjs | 99 ++++++++ .../new-candidate-instant.mjs | 90 +++++++ .../new-contact-instant.mjs | 89 +++++++ 8 files changed, 849 insertions(+), 5 deletions(-) create mode 100644 components/cats/actions/add-candidate-pipeline/add-candidate-pipeline.mjs create mode 100644 components/cats/actions/create-candidate/create-candidate.mjs create mode 100644 components/cats/actions/create-contact/create-contact.mjs create mode 100644 components/cats/sources/new-activity-instant/new-activity-instant.mjs create mode 100644 components/cats/sources/new-candidate-instant/new-candidate-instant.mjs create mode 100644 components/cats/sources/new-contact-instant/new-contact-instant.mjs diff --git a/components/cats/actions/add-candidate-pipeline/add-candidate-pipeline.mjs b/components/cats/actions/add-candidate-pipeline/add-candidate-pipeline.mjs new file mode 100644 index 0000000000000..3d33f8f2ba33f --- /dev/null +++ b/components/cats/actions/add-candidate-pipeline/add-candidate-pipeline.mjs @@ -0,0 +1,45 @@ +import cats from "../../cats.app.mjs"; + +export default { + key: "cats-add-candidate-pipeline", + name: "Add Candidate to Job Pipeline", + description: "Adds a specific candidate to a job pipeline in CATS. [See the documentation](https://docs.catsone.com/api/v3/)", + version: "0.0.{{ts}}", + type: "action", + props: { + cats, + candidateId: { + propDefinition: [ + cats, + "candidateId", + ], + }, + jobId: { + propDefinition: [ + cats, + "jobId", + ], + }, + rating: { + propDefinition: [ + cats, + "rating", + ], + optional: true, + }, + }, + async run({ $ }) { + const data = { + candidate_id: this.candidateId, + job_id: this.jobId, + }; + + if (this.rating !== undefined) { + data.rating = this.rating; + } + + const response = await this.cats.addCandidateToJobPipeline(data); + $.export("$summary", `Successfully added candidate ID ${this.candidateId} to job pipeline ID ${this.jobId}`); + return response; + }, +}; diff --git a/components/cats/actions/create-candidate/create-candidate.mjs b/components/cats/actions/create-candidate/create-candidate.mjs new file mode 100644 index 0000000000000..222f47a84910f --- /dev/null +++ b/components/cats/actions/create-candidate/create-candidate.mjs @@ -0,0 +1,184 @@ +import cats from "../../cats.app.mjs"; + +export default { + key: "cats-create-candidate", + name: "Create Candidate", + description: "Create a new candidate in your CATS database. [See the documentation](https://docs.catsone.com/api/v3/#create-a-candidate)", + version: "0.0.{{ts}}", + type: "action", + props: { + cats, + checkDuplicate: { + propDefinition: [ + cats, + "checkDuplicate", + ], + }, + firstName: { + propDefinition: [ + cats, + "firstName", + ], + }, + lastName: { + propDefinition: [ + cats, + "lastName", + ], + }, + middleName: { + propDefinition: [ + cats, + "middleName", + ], + optional: true, + }, + title: { + propDefinition: [ + cats, + "title", + ], + optional: true, + }, + phones: { + propDefinition: [ + cats, + "phones", + ], + optional: true, + }, + address: { + propDefinition: [ + cats, + "address", + ], + optional: true, + }, + countryCode: { + propDefinition: [ + cats, + "countryCode", + ], + optional: true, + }, + socialMediaUrls: { + propDefinition: [ + cats, + "socialMediaUrls", + ], + optional: true, + }, + website: { + propDefinition: [ + cats, + "website", + ], + optional: true, + }, + bestTimeToCall: { + propDefinition: [ + cats, + "bestTimeToCall", + ], + optional: true, + }, + currentEmployer: { + propDefinition: [ + cats, + "currentEmployer", + ], + optional: true, + }, + dateAvailable: { + propDefinition: [ + cats, + "dateAvailable", + ], + optional: true, + }, + desiredPay: { + propDefinition: [ + cats, + "desiredPay", + ], + optional: true, + }, + isWillingToRelocate: { + propDefinition: [ + cats, + "isWillingToRelocate", + ], + optional: true, + }, + keySkills: { + propDefinition: [ + cats, + "keySkills", + ], + optional: true, + }, + notes: { + propDefinition: [ + cats, + "notes", + ], + optional: true, + }, + source: { + propDefinition: [ + cats, + "source", + ], + optional: true, + }, + ownerId: { + propDefinition: [ + cats, + "ownerId", + ], + optional: true, + }, + workHistory: { + propDefinition: [ + cats, + "workHistory", + ], + optional: true, + }, + }, + async run({ $ }) { + const candidateData = { + check_duplicate: this.checkDuplicate, + first_name: this.firstName, + last_name: this.lastName, + middle_name: this.middleName, + title: this.title, + phones: this.phones + ? this.phones.map(JSON.parse) + : undefined, + address: this.address, + country_code: this.countryCode, + social_media_urls: this.socialMediaUrls + ? this.socialMediaUrls.map(JSON.parse) + : undefined, + website: this.website, + best_time_to_call: this.bestTimeToCall, + current_employer: this.currentEmployer, + date_available: this.dateAvailable, + desired_pay: this.desiredPay, + is_willing_to_relocate: this.isWillingToRelocate, + key_skills: this.keySkills, + notes: this.notes, + source: this.source, + owner_id: this.ownerId, + work_history: this.workHistory + ? this.workHistory.map(JSON.parse) + : undefined, + }; + + const response = await this.cats.createCandidate(candidateData); + + $.export("$summary", `Created candidate with ID ${response.id}`); + return response; + }, +}; diff --git a/components/cats/actions/create-contact/create-contact.mjs b/components/cats/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..f1056e10ea544 --- /dev/null +++ b/components/cats/actions/create-contact/create-contact.mjs @@ -0,0 +1,121 @@ +import cats from "../../cats.app.mjs"; +import { axios } from "@pipedream/platform"; + +export default { + key: "cats-create-contact", + name: "Create Contact", + description: "Adds a new contact to the CATS platform. [See the documentation](https://docs.catsone.com/api/v3/)", + version: "0.0.{{ts}}", + type: "action", + props: { + cats, + firstName: { + propDefinition: [ + cats, + "firstName", + ], + }, + lastName: { + propDefinition: [ + cats, + "lastName", + ], + }, + ownerId: { + propDefinition: [ + cats, + "ownerId", + ], + }, + companyId: { + propDefinition: [ + cats, + "companyId", + ], + }, + title: { + propDefinition: [ + cats, + "title", + ], + optional: true, + }, + emails: { + propDefinition: [ + cats, + "emails", + ], + optional: true, + }, + phones: { + propDefinition: [ + cats, + "phones", + ], + optional: true, + }, + address: { + propDefinition: [ + cats, + "address", + ], + optional: true, + }, + countryCode: { + propDefinition: [ + cats, + "countryCode", + ], + optional: true, + }, + socialMediaUrls: { + propDefinition: [ + cats, + "socialMediaUrls", + ], + optional: true, + }, + isHot: { + propDefinition: [ + cats, + "isHot", + ], + optional: true, + }, + notes: { + propDefinition: [ + cats, + "notes", + ], + optional: true, + }, + customFields: { + propDefinition: [ + cats, + "customFields", + ], + optional: true, + }, + }, + async run({ $ }) { + const data = { + first_name: this.firstName, + last_name: this.lastName, + owner_id: this.ownerId, + company_id: this.companyId, + title: this.title, + emails: this.emails, + phones: this.phones, + address: this.address, + country_code: this.countryCode, + social_media_urls: this.socialMediaUrls, + is_hot: this.isHot, + notes: this.notes, + custom_fields: this.customFields, + }; + + const response = await this.cats.createContact(data); + $.export("$summary", `Contact created: ${response.id}`); + return response; + }, +}; diff --git a/components/cats/cats.app.mjs b/components/cats/cats.app.mjs index 35105682c0dee..972a1f2b6bc86 100644 --- a/components/cats/cats.app.mjs +++ b/components/cats/cats.app.mjs @@ -1,11 +1,227 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "cats", - propDefinitions: {}, + propDefinitions: { + checkDuplicate: { + type: "string", + label: "Check Duplicate", + description: "Whether to check for duplicates when creating a candidate", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the candidate", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the candidate", + }, + middleName: { + type: "string", + label: "Middle Name", + description: "The middle name of the candidate", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "The candidate's job title", + optional: true, + }, + phones: { + type: "string[]", + label: "Phones", + description: "An array of phone objects", + optional: true, + }, + address: { + type: "object", + label: "Address", + description: "The address of the candidate", + optional: true, + }, + countryCode: { + type: "string", + label: "Country Code", + description: "The country code of the candidate", + optional: true, + }, + socialMediaUrls: { + type: "string[]", + label: "Social Media URLs", + description: "The social media URLs of the candidate", + optional: true, + }, + website: { + type: "string", + label: "Website", + description: "The website of the candidate", + optional: true, + }, + bestTimeToCall: { + type: "string", + label: "Best Time to Call", + description: "The best time to call the candidate", + optional: true, + }, + currentEmployer: { + type: "string", + label: "Current Employer", + description: "The current employer of the candidate", + optional: true, + }, + dateAvailable: { + type: "string", + label: "Date Available", + description: "The date the candidate is available for an opening", + optional: true, + }, + desiredPay: { + type: "string", + label: "Desired Pay", + description: "The desired pay for the candidate", + optional: true, + }, + isWillingToRelocate: { + type: "boolean", + label: "Willing to Relocate", + description: "Whether the candidate is willing to relocate", + optional: true, + }, + keySkills: { + type: "string", + label: "Key Skills", + description: "The key skills of the candidate", + optional: true, + }, + notes: { + type: "string", + label: "Notes", + description: "Any notes about the candidate", + optional: true, + }, + source: { + type: "string", + label: "Source", + description: "The source of the candidate", + optional: true, + }, + ownerId: { + type: "number", + label: "Owner ID", + description: "The user id of the record owner", + optional: true, + }, + workHistory: { + type: "string[]", + label: "Work History", + description: "An array of work history objects", + optional: true, + }, + candidateId: { + type: "number", + label: "Candidate ID", + description: "The ID of the candidate", + }, + jobId: { + type: "number", + label: "Job ID", + description: "The ID of the job to which the candidate is added", + }, + rating: { + type: "number", + label: "Rating", + description: "The candidate's rating for the job (0-5)", + optional: true, + }, + companyId: { + type: "number", + label: "Company ID", + description: "The company ID related to the contact", + }, + emails: { + type: "string[]", + label: "Emails", + description: "An array of email objects", + optional: true, + }, + isHot: { + type: "boolean", + label: "Is Hot", + description: "Whether the contact is marked as hot", + optional: true, + }, + customFields: { + type: "string[]", + label: "Custom Fields", + description: "An array of custom field objects", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.catsone.com/v3"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, method = "GET", path = "/", headers, ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + method, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + async createCandidate(data) { + const result = await this._makeRequest({ + method: "POST", + path: "/candidates", + data, + }); + this.$emit(result, { + summary: "Candidate created", + name: "candidate.created", + }); + return result; + }, + async addCandidateToJobPipeline(data) { + return this._makeRequest({ + method: "POST", + path: "/pipelines", + data, + }); + }, + async createContact(data) { + const result = await this._makeRequest({ + method: "POST", + path: "/contacts", + data, + }); + this.$emit(result, { + summary: "Contact created", + name: "contact.created", + }); + return result; + }, + async createActivity(data) { + const result = await this._makeRequest({ + method: "POST", + path: "/activities", + data, + }); + this.$emit(result, { + summary: "Activity created", + name: "activity.created", + }); + return result; }, }, + version: "0.0.{{ts}}", }; diff --git a/components/cats/package.json b/components/cats/package.json index 08d007c23d152..efdc957605db6 100644 --- a/components/cats/package.json +++ b/components/cats/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/cats/sources/new-activity-instant/new-activity-instant.mjs b/components/cats/sources/new-activity-instant/new-activity-instant.mjs new file mode 100644 index 0000000000000..8589524d44cee --- /dev/null +++ b/components/cats/sources/new-activity-instant/new-activity-instant.mjs @@ -0,0 +1,99 @@ +import cats from "../../cats.app.mjs"; +import crypto from "crypto"; +import { axios } from "@pipedream/platform"; + +export default { + key: "cats-new-activity-instant", + name: "New Activity Instant", + description: "Emit a new event when an activity related to a cat is created. [See the documentation](https://docs.catsone.com/api/v3/)", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + cats, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + }, + methods: { + _getWebhookId() { + return this.db.get("webhookId"); + }, + _setWebhookId(id) { + this.db.set("webhookId", id); + }, + }, + hooks: { + async deploy() { + const activities = await this.cats._makeRequest({ + path: "/activities", + params: { + limit: 50, + order_by: "created_at.desc", + }, + }); + for (const activity of activities) { + this.$emit(activity, { + id: activity.id, + summary: `New activity: ${activity.type}`, + ts: Date.parse(activity.created_at), + }); + } + }, + async activate() { + const webhookConfig = { + events: [ + "activity.created", + ], + target_url: this.http.endpoint, + secret: process.env.CATS_WEBHOOK_SECRET, // Assuming secret is stored as environment variable + }; + const webhookResponse = await this.cats._makeRequest({ + method: "POST", + path: "/webhooks", + data: webhookConfig, + }); + this._setWebhookId(webhookResponse.id); + }, + async deactivate() { + const webhookId = this._getWebhookId(); + if (webhookId) { + await this.cats._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + this.db.set("webhookId", null); + } + }, + }, + async run(event) { + const secret = process.env.CATS_WEBHOOK_SECRET; // Assuming secret is stored as environment variable + const signature = event.headers["x-signature"]; + const requestId = event.headers["x-request-id"]; + const payload = JSON.stringify(event.body) + requestId; + const computedSignature = `HMAC-SHA256 ${crypto + .createHmac("sha256", secret) + .update(payload) + .digest("hex")}`; + + if (computedSignature !== signature) { + this.http.respond({ + status: 401, + body: "Unauthorized", + }); + return; + } + + this.http.respond({ + status: 200, + }); + const activity = event.body; + this.$emit(activity, { + id: activity.id, + summary: `New activity: ${activity.type}`, + ts: Date.parse(activity.created_at), + }); + }, +}; diff --git a/components/cats/sources/new-candidate-instant/new-candidate-instant.mjs b/components/cats/sources/new-candidate-instant/new-candidate-instant.mjs new file mode 100644 index 0000000000000..2ed742d64ecc6 --- /dev/null +++ b/components/cats/sources/new-candidate-instant/new-candidate-instant.mjs @@ -0,0 +1,90 @@ +import cats from "../../cats.app.mjs"; +import crypto from "crypto"; +import { axios } from "@pipedream/platform"; + +export default { + key: "cats-new-candidate-instant", + name: "New Candidate Instant", + description: "Emit a new event when a new candidate is created. [See the documentation](https://docs.catsone.com/api/v3/)", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + cats, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + }, + methods: { + _getWebhookId() { + return this.db.get("webhookId"); + }, + _setWebhookId(id) { + this.db.set("webhookId", id); + }, + }, + hooks: { + async deploy() { + // Fetching the most recent candidate events is not required as per the provided instructions + }, + async activate() { + const webhookConfig = { + events: [ + "candidate.created", + ], + target_url: this.http.endpoint, + secret: this.cats.$auth.api_key, // Using the API key as the secret for signature validation + }; + + const response = await axios(this, { + method: "POST", + url: `${this.cats._baseUrl()}/webhooks`, + headers: { + Authorization: `Bearer ${this.cats.$auth.api_key}`, + }, + data: webhookConfig, + }); + + const webhookId = response.id; + this._setWebhookId(webhookId); + }, + async deactivate() { + const webhookId = this._getWebhookId(); + if (webhookId) { + await axios(this, { + method: "DELETE", + url: `${this.cats._baseUrl()}/webhooks/${webhookId}`, + headers: { + Authorization: `Bearer ${this.cats.$auth.api_key}`, + }, + }); + } + }, + }, + async run(event) { + const signature = event.headers["X-Signature"]; + const rawBody = JSON.stringify(event.body); + const computedSignature = `HMAC-SHA256 ${crypto.createHmac("sha256", this.cats.$auth.api_key).update(rawBody) + .digest("hex")}`; + + if (computedSignature !== signature) { + this.http.respond({ + status: 401, + body: "Unauthorized", + }); + return; + } + + this.http.respond({ + status: 200, + body: "OK", + }); + this.$emit(event.body, { + id: event.body.id, + summary: `New candidate created: ${event.body.first_name} ${event.body.last_name}`, + ts: Date.now(), + }); + }, +}; diff --git a/components/cats/sources/new-contact-instant/new-contact-instant.mjs b/components/cats/sources/new-contact-instant/new-contact-instant.mjs new file mode 100644 index 0000000000000..cd85a6d04c541 --- /dev/null +++ b/components/cats/sources/new-contact-instant/new-contact-instant.mjs @@ -0,0 +1,89 @@ +import cats from "../../cats.app.mjs"; +import crypto from "crypto"; +import { axios } from "@pipedream/platform"; + +export default { + key: "cats-new-contact-instant", + name: "New Contact Created", + description: "Emit a new event when a contact related to a cat is created. [See the documentation](https://docs.catsone.com/api/v3/)", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + cats: { + type: "app", + app: "cats", + }, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + }, + hooks: { + async deploy() { + const contacts = await this.cats._makeRequest({ + path: "/contacts", + params: { + limit: 50, + orderBy: "created_at.desc", + }, + }); + for (const contact of contacts) { + this.$emit(contact, { + id: contact.id, + summary: `New contact: ${contact.firstName} ${contact.lastName}`, + ts: Date.parse(contact.createdAt), + }); + } + }, + async activate() { + const webhookData = { + events: [ + "contact.created", + ], + target_url: this.http.endpoint, + secret: this.db.get("secret"), + }; + const response = await this.cats._makeRequest({ + method: "POST", + path: "/webhooks", + data: webhookData, + }); + this.db.set("webhookId", response.id); + }, + async deactivate() { + const webhookId = this.db.get("webhookId"); + if (webhookId) { + await this.cats._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + } + }, + }, + async run(event) { + const webhookSignature = event.headers["x-signature"]; + const requestId = event.headers["x-request-id"]; + const secret = this.db.get("secret") || "your_secret_key"; + const rawBody = event.rawBody; + + const computedSignature = `HMAC-SHA256 ${crypto.createHmac("sha256", secret).update(rawBody + requestId) + .digest("hex")}`; + + if (computedSignature !== webhookSignature) { + this.http.respond({ + status: 401, + body: "Unauthorized", + }); + return; + } + + const contact = event.body; + this.$emit(contact, { + id: contact.id, + summary: `New contact: ${contact.firstName} ${contact.lastName}`, + ts: Date.parse(contact.createdAt), + }); + }, +}; From f26fdfc3b1b3fe0a8de41ece0b2a1e14762f69d2 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Tue, 19 Nov 2024 17:25:01 -0300 Subject: [PATCH 2/3] [Components] cats #14489 Sources - New Candidate (Instant) - New Contact (Instant) - New Activity (Instant) Actions - Create Candidate - Add Candidate Pipeline - Create Contact --- .../add-candidate-pipeline.mjs | 45 +- .../create-candidate/create-candidate.mjs | 218 +++++++--- .../actions/create-contact/create-contact.mjs | 151 +++++-- components/cats/cats.app.mjs | 385 ++++++++++++------ components/cats/common/utils.mjs | 24 ++ components/cats/package.json | 6 +- components/cats/sources/common/base.mjs | 80 ++++ .../new-activity-instant.mjs | 105 +---- .../new-activity-instant/test-event.mjs | 41 ++ .../new-candidate-instant.mjs | 97 +---- .../new-candidate-instant/test-event.mjs | 112 +++++ .../new-contact-instant.mjs | 98 +---- .../new-contact-instant/test-event.mjs | 103 +++++ 13 files changed, 997 insertions(+), 468 deletions(-) create mode 100644 components/cats/common/utils.mjs create mode 100644 components/cats/sources/common/base.mjs create mode 100644 components/cats/sources/new-activity-instant/test-event.mjs create mode 100644 components/cats/sources/new-candidate-instant/test-event.mjs create mode 100644 components/cats/sources/new-contact-instant/test-event.mjs diff --git a/components/cats/actions/add-candidate-pipeline/add-candidate-pipeline.mjs b/components/cats/actions/add-candidate-pipeline/add-candidate-pipeline.mjs index 3d33f8f2ba33f..92e7914068faf 100644 --- a/components/cats/actions/add-candidate-pipeline/add-candidate-pipeline.mjs +++ b/components/cats/actions/add-candidate-pipeline/add-candidate-pipeline.mjs @@ -3,11 +3,17 @@ import cats from "../../cats.app.mjs"; export default { key: "cats-add-candidate-pipeline", name: "Add Candidate to Job Pipeline", - description: "Adds a specific candidate to a job pipeline in CATS. [See the documentation](https://docs.catsone.com/api/v3/)", - version: "0.0.{{ts}}", + description: "Adds a specific candidate to a job pipeline in CATS. [See the documentation](https://docs.catsone.com/api/v3/#jobs-create-a-job)", + version: "0.0.1", type: "action", props: { cats, + createActivity: { + type: "boolean", + label: "Create Activity", + description: "Whether a corresponding activity should be created automatically. This mimics what happens when a pipeline is created from the CATS UI.", + optional: true, + }, candidateId: { propDefinition: [ cats, @@ -21,25 +27,32 @@ export default { ], }, rating: { - propDefinition: [ - cats, - "rating", - ], + type: "integer", + label: "Rating", + description: "The record's rating for the job (0-5).", optional: true, }, }, async run({ $ }) { - const data = { - candidate_id: this.candidateId, - job_id: this.jobId, - }; + const { headers } = await this.cats.addCandidateToJobPipeline({ + $, + returnFullResponse: true, + params: { + create_activity: this.createActivity, + }, + data: { + candidate_id: this.candidateId, + job_id: this.jobId, + rating: this.rating, + }, + }); - if (this.rating !== undefined) { - data.rating = this.rating; - } + const location = headers.location.split("/"); + const pipelineId = location[location.length - 1]; - const response = await this.cats.addCandidateToJobPipeline(data); - $.export("$summary", `Successfully added candidate ID ${this.candidateId} to job pipeline ID ${this.jobId}`); - return response; + $.export("$summary", `Successfully added candidate ID ${this.candidateId} to job ID ${this.jobId}`); + return { + pipelineId, + }; }, }; diff --git a/components/cats/actions/create-candidate/create-candidate.mjs b/components/cats/actions/create-candidate/create-candidate.mjs index 222f47a84910f..6dd807516ebfe 100644 --- a/components/cats/actions/create-candidate/create-candidate.mjs +++ b/components/cats/actions/create-candidate/create-candidate.mjs @@ -1,10 +1,11 @@ import cats from "../../cats.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; export default { key: "cats-create-candidate", name: "Create Candidate", - description: "Create a new candidate in your CATS database. [See the documentation](https://docs.catsone.com/api/v3/#create-a-candidate)", - version: "0.0.{{ts}}", + description: "Create a new candidate in your CATS database. [See the documentation](https://docs.catsone.com/api/v3/#candidates-create-a-candidate)", + version: "0.0.1", type: "action", props: { cats, @@ -20,23 +21,30 @@ export default { "firstName", ], }, + middleName: { + propDefinition: [ + cats, + "middleName", + ], + optional: true, + }, lastName: { propDefinition: [ cats, "lastName", ], }, - middleName: { + title: { propDefinition: [ cats, - "middleName", + "title", ], optional: true, }, - title: { + emails: { propDefinition: [ cats, - "title", + "emails", ], optional: true, }, @@ -47,10 +55,31 @@ export default { ], optional: true, }, - address: { + addressStreet: { + propDefinition: [ + cats, + "addressStreet", + ], + optional: true, + }, + addressCity: { + propDefinition: [ + cats, + "addressCity", + ], + optional: true, + }, + addressState: { + propDefinition: [ + cats, + "addressState", + ], + optional: true, + }, + addressPostalCode: { propDefinition: [ cats, - "address", + "addressPostalCode", ], optional: true, }, @@ -69,10 +98,9 @@ export default { optional: true, }, website: { - propDefinition: [ - cats, - "website", - ], + type: "string", + label: "Website", + description: "The website of the record.", optional: true, }, bestTimeToCall: { @@ -90,10 +118,15 @@ export default { optional: true, }, dateAvailable: { - propDefinition: [ - cats, - "dateAvailable", - ], + type: "string", + label: "Date Available", + description: "The date the record is available for an opening. **Format: YYYY-MM-DD**.", + optional: true, + }, + currentPay: { + type: "string", + label: "Current Pay", + description: "The current pay of the record.", optional: true, }, desiredPay: { @@ -138,6 +171,35 @@ export default { ], optional: true, }, + isActive: { + type: "boolean", + label: "Is Active", + description: "A flag indicating if the candidate is active.", + optional: true, + }, + isHot: { + propDefinition: [ + cats, + "isHot", + ], + optional: true, + }, + password: { + type: "string", + label: "password", + description: "The candidate's password if they are \"registering\".", + secret: true, + optional: true, + }, + customFields: { + propDefinition: [ + cats, + "customFields", + ], + withLabel: true, + reloadProps: true, + optional: true, + }, workHistory: { propDefinition: [ cats, @@ -146,39 +208,101 @@ export default { optional: true, }, }, + async additionalProps() { + const props = {}; + (this.customFields || []).map(({ + label, value, + }) => { + props[value] = { + type: "string", + label: `Custom Field: ${label}`, + optional: true, + }; + }, {}); + + return props; + }, async run({ $ }) { - const candidateData = { - check_duplicate: this.checkDuplicate, - first_name: this.firstName, - last_name: this.lastName, - middle_name: this.middleName, - title: this.title, - phones: this.phones - ? this.phones.map(JSON.parse) - : undefined, - address: this.address, - country_code: this.countryCode, - social_media_urls: this.socialMediaUrls - ? this.socialMediaUrls.map(JSON.parse) - : undefined, - website: this.website, - best_time_to_call: this.bestTimeToCall, - current_employer: this.currentEmployer, - date_available: this.dateAvailable, - desired_pay: this.desiredPay, - is_willing_to_relocate: this.isWillingToRelocate, - key_skills: this.keySkills, - notes: this.notes, - source: this.source, - owner_id: this.ownerId, - work_history: this.workHistory - ? this.workHistory.map(JSON.parse) - : undefined, - }; + const { + cats, // eslint-disable-next-line no-unused-vars + customFields, + firstName, + lastName, + ownerId, + middleName, + checkDuplicate, + bestTimeToCall, + currentEmployer, + emails, + phones, + addressStreet, + addressCity, + addressState, + addressPostalCode, + countryCode, + socialMediaUrls, + dateAvailable, + currentPay, + desiredPay, + isWillingToRelocate, + keySkills, + isActive, + isHot, + workHistory, + ...data + } = this; - const response = await this.cats.createCandidate(candidateData); + const customFieldsObject = customFields + ? customFields.map(({ value }) => { + return { + id: value, + value: data[value], + }; + }) + : {}; - $.export("$summary", `Created candidate with ID ${response.id}`); - return response; + const { headers } = await cats.createCandidate({ + $, + returnFullResponse: true, + params: { + check_duplicate: checkDuplicate, + }, + data: { + first_name: firstName, + middle_name: middleName, + last_name: lastName, + emails: parseObject(emails), + phones: parseObject(phones), + address: { + street: addressStreet, + city: addressCity, + state: addressState, + postal_code: addressPostalCode, + }, + country_code: countryCode, + social_media_urls: parseObject(socialMediaUrls), + best_time_to_call: bestTimeToCall, + current_employer: currentEmployer, + date_available: dateAvailable, + current_pay: currentPay, + desired_pay: desiredPay, + is_willing_to_relocate: isWillingToRelocate, + key_skills: keySkills, + owner_id: ownerId, + is_active: isActive, + is_hot: isHot, + work_history: parseObject(workHistory), + custom_fields: customFieldsObject, + ...data, + }, + }); + + const location = headers.location.split("/"); + const candidateId = location[location.length - 1]; + + $.export("$summary", `Created candidate with ID ${candidateId}`); + return { + candidateId, + }; }, }; diff --git a/components/cats/actions/create-contact/create-contact.mjs b/components/cats/actions/create-contact/create-contact.mjs index f1056e10ea544..e1ffe64da8b38 100644 --- a/components/cats/actions/create-contact/create-contact.mjs +++ b/components/cats/actions/create-contact/create-contact.mjs @@ -1,11 +1,11 @@ import cats from "../../cats.app.mjs"; -import { axios } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; export default { key: "cats-create-contact", name: "Create Contact", - description: "Adds a new contact to the CATS platform. [See the documentation](https://docs.catsone.com/api/v3/)", - version: "0.0.{{ts}}", + description: "Adds a new contact to the CATS platform. [See the documentation](https://docs.catsone.com/api/v3/#contacts-create-a-contact)", + version: "0.0.1", type: "action", props: { cats, @@ -33,6 +33,13 @@ export default { "companyId", ], }, + checkDuplicate: { + propDefinition: [ + cats, + "checkDuplicate", + ], + optional: true, + }, title: { propDefinition: [ cats, @@ -40,6 +47,21 @@ export default { ], optional: true, }, + reportsToId: { + propDefinition: [ + cats, + "contactId", + ], + label: "Reports To Id", + description: "The ID of the contact that this contact reports to.", + optional: true, + }, + hasLeftCompany: { + type: "boolean", + label: "Has Left Company", + description: "Whether the contact has left the company or not.", + optional: true, + }, emails: { propDefinition: [ cats, @@ -54,10 +76,31 @@ export default { ], optional: true, }, - address: { + addressStreet: { + propDefinition: [ + cats, + "addressStreet", + ], + optional: true, + }, + addressCity: { propDefinition: [ cats, - "address", + "addressCity", + ], + optional: true, + }, + addressState: { + propDefinition: [ + cats, + "addressState", + ], + optional: true, + }, + addressPostalCode: { + propDefinition: [ + cats, + "addressPostalCode", ], optional: true, }, @@ -94,28 +137,90 @@ export default { cats, "customFields", ], + withLabel: true, + reloadProps: true, optional: true, }, }, + async additionalProps() { + const props = {}; + (this.customFields || []).map(({ + label, value, + }) => { + props[value] = { + type: "string", + label: `Custom Field: ${label}`, + optional: true, + }; + }, {}); + + return props; + }, async run({ $ }) { - const data = { - first_name: this.firstName, - last_name: this.lastName, - owner_id: this.ownerId, - company_id: this.companyId, - title: this.title, - emails: this.emails, - phones: this.phones, - address: this.address, - country_code: this.countryCode, - social_media_urls: this.socialMediaUrls, - is_hot: this.isHot, - notes: this.notes, - custom_fields: this.customFields, - }; + const { + cats, // eslint-disable-next-line no-unused-vars + customFields, + firstName, + lastName, + ownerId, + companyId, + checkDuplicate, + reportsToId, + hasLeftCompany, + emails, + phones, + addressStreet, + addressCity, + addressState, + addressPostalCode, + countryCode, + socialMediaUrls, + ...data + } = this; - const response = await this.cats.createContact(data); - $.export("$summary", `Contact created: ${response.id}`); - return response; + const customFieldsObject = customFields + ? customFields.map(({ value }) => { + return { + id: value, + value: data[value], + }; + }) + : {}; + + const { headers } = await cats.createContact({ + $, + returnFullResponse: true, + params: { + check_duplicate: checkDuplicate, + }, + data: { + first_name: firstName, + last_name: lastName, + owner_id: ownerId, + company_id: companyId, + reports_to_id: reportsToId, + has_left_company: hasLeftCompany, + emails: parseObject(emails), + phones: parseObject(phones), + address: { + street: addressStreet, + city: addressCity, + state: addressState, + postal_code: addressPostalCode, + }, + country_code: countryCode, + social_media_urls: parseObject(socialMediaUrls), + custom_fields: customFieldsObject, + ...data, + }, + }); + + const location = headers.location.split("/"); + const contactId = location[location.length - 1]; + + $.export("$summary", `Contact successfully created with Id: ${contactId}!`); + return { + contactId, + }; }, }; diff --git a/components/cats/cats.app.mjs b/components/cats/cats.app.mjs index 972a1f2b6bc86..74694a4ba14ba 100644 --- a/components/cats/cats.app.mjs +++ b/components/cats/cats.app.mjs @@ -4,224 +4,339 @@ export default { type: "app", app: "cats", propDefinitions: { - checkDuplicate: { - type: "string", - label: "Check Duplicate", - description: "Whether to check for duplicates when creating a candidate", + candidateId: { + type: "integer", + label: "Candidate ID", + description: "The ID of the candidate.", + async options({ page }) { + const { _embedded } = await this.listCandidates({ + params: { + page: page + 1, + }, + }); + + return _embedded + ? _embedded.candidates.map(({ + id, first_name: fName, last_name: lName, emails: { + primary, secondary, + }, + }) => ({ + label: `${fName} ${lName} ${primary || secondary + ? `(${primary || secondary})` + : ""}`, + value: parseInt(id), + })) + : []; + }, + }, + companyId: { + type: "integer", + label: "Company ID", + description: "The company ID related to the contact.", + async options({ page }) { + const { _embedded } = await this.listCompanies({ + params: { + page: page + 1, + }, + }); + + return _embedded + ? _embedded.companies.map(({ + id: value, name: label, + }) => ({ + label, + value, + })) + : []; + }, + }, + contactId: { + type: "integer", + label: "Contact Id", + description: "The ID of the contact.", + async options({ page }) { + const { _embedded } = await this.listContacts({ + params: { + page: page + 1, + }, + }); + + return _embedded + ? _embedded.contacts.map(({ + id, first_name: fName, last_name: lName, emails: { primary }, + }) => ({ + label: `${fName} ${lName} ${primary + ? `(${primary})` + : ""}`, + value: parseInt(id), + })) + : []; + }, + }, + ownerId: { + type: "integer", + label: "Owner ID", + description: "The user id of the record owner.", + async options({ page }) { + const { _embedded } = await this.listUsers({ + params: { + page: page + 1, + }, + }); + + return _embedded + ? _embedded.users.map(({ + id: value, username: label, + }) => ({ + label, + value, + })) + : []; + }, + }, + customFields: { + type: "string[]", + label: "Custom Fields", + description: "An array of custom field objects. ", + async options({ page }) { + const { _embedded } = await this.listCustomFields({ + params: { + page: page + 1, + }, + }); + + return _embedded + ? _embedded.custom_fields.map(({ + id: value, name: label, + }) => ({ + label, + value, + })) + : []; + }, + }, + jobId: { + type: "integer", + label: "Job ID", + description: "The ID of the job to which the record is added.", + async options({ page }) { + const { _embedded } = await this.listJobs({ + params: { + page: page + 1, + }, + }); + + return _embedded + ? _embedded.jobs.map(({ + id: value, title: label, + }) => ({ + label, + value, + })) + : []; + }, }, firstName: { type: "string", label: "First Name", - description: "The first name of the candidate", + description: "The first name of the record.", + }, + middleName: { + type: "string", + label: "Middle Name", + description: "The middle name of the record.", }, lastName: { type: "string", label: "Last Name", - description: "The last name of the candidate", + description: "The last name of the record.", }, - middleName: { - type: "string", - label: "Middle Name", - description: "The middle name of the candidate", - optional: true, + checkDuplicate: { + type: "boolean", + label: "Check Duplicate", + description: "When this flag is set to true, if a duplicate record is found to the one being created, an error will be thrown instead of creating a duplicate record.", + default: false, }, title: { type: "string", label: "Title", - description: "The candidate's job title", - optional: true, + description: "The record's job title.", + }, + emails: { + type: "string[]", + label: "Emails", + description: "An array of email objects. Each email object should contain two keys: `email` and `is_primary`, as described [here](https://docs.catsone.com/api/v3/#contacts-create-a-contact-email). **Format: {\"email\":\"example@email.com\", \"is_primary\":\"true\"}**", + }, + isHot: { + type: "boolean", + label: "Is Hot", + description: "Whether the record is marked as hot.", }, phones: { type: "string[]", label: "Phones", - description: "An array of phone objects", - optional: true, + description: "An array of phone objects. Each phone object should contain three keys: `number`, `extension`, and `type`, as described [here](https://docs.catsone.com/api/v3/#contacts-create-a-contact-phone). **Format: {\"number\":\"+16124063451\", \"extension\":\"8371\", \"type\":\"mobile\"}**. Type can be `mobile`, `home`, `work`, `fax`, `main` or `other`", + }, + addressStreet: { + type: "string", + label: "Street Address", + description: "The street of the record's address.", }, - address: { - type: "object", - label: "Address", - description: "The address of the candidate", - optional: true, + addressCity: { + type: "string", + label: "City Address", + description: "The city of the record's address.", + }, + addressState: { + type: "string", + label: "State Address", + description: "The state of the record's address.", + }, + addressPostalCode: { + type: "string", + label: "Address Postal Code", + description: "The postal code of the record's address.", }, countryCode: { type: "string", label: "Country Code", - description: "The country code of the candidate", - optional: true, + description: "The country code of the record.", }, socialMediaUrls: { type: "string[]", label: "Social Media URLs", - description: "The social media URLs of the candidate", - optional: true, + description: "The social media URLs of the record.", }, - website: { + notes: { type: "string", - label: "Website", - description: "The website of the candidate", - optional: true, + label: "Notes", + description: "Any notes about the record.", }, bestTimeToCall: { type: "string", label: "Best Time to Call", - description: "The best time to call the candidate", - optional: true, + description: "The best time to call the record. **Format: HH:MM**.", }, currentEmployer: { type: "string", label: "Current Employer", - description: "The current employer of the candidate", - optional: true, - }, - dateAvailable: { - type: "string", - label: "Date Available", - description: "The date the candidate is available for an opening", - optional: true, + description: "The current employer of the record.", }, desiredPay: { type: "string", label: "Desired Pay", - description: "The desired pay for the candidate", - optional: true, + description: "The desired pay for the record.", }, isWillingToRelocate: { type: "boolean", label: "Willing to Relocate", - description: "Whether the candidate is willing to relocate", - optional: true, + description: "Whether the record is willing to relocate.", }, keySkills: { type: "string", label: "Key Skills", - description: "The key skills of the candidate", - optional: true, - }, - notes: { - type: "string", - label: "Notes", - description: "Any notes about the candidate", - optional: true, + description: "The key skills of the record.", }, source: { type: "string", label: "Source", - description: "The source of the candidate", - optional: true, - }, - ownerId: { - type: "number", - label: "Owner ID", - description: "The user id of the record owner", - optional: true, + description: "The source of the record.", }, workHistory: { type: "string[]", label: "Work History", - description: "An array of work history objects", - optional: true, + description: "An array of work history objects. **Example: {\"title\": \"Engineer\", \"employer\": { \"linked\": false, \"name\": \"\", \"location\": { \"city\": \"\", \"state\": \"\" }}, \"supervisor\": { \"linked\": false, \"name\": \"\", \"phone\": \"\" }, \"is_verified\": true, \"is_current\": false, \"start_date\": \"YYYY-MM-DD\", \"end_date\": \"YYYY-MM-DD\", \"reason_for_leaving\": \"foo\"}**", }, - candidateId: { - type: "number", - label: "Candidate ID", - description: "The ID of the candidate", + }, + methods: { + _baseUrl() { + return "https://api.catsone.com/v3"; }, - jobId: { - type: "number", - label: "Job ID", - description: "The ID of the job to which the candidate is added", + _headers() { + return { + Authorization: `Token ${this.$auth.api_key}`, + }; }, - rating: { - type: "number", - label: "Rating", - description: "The candidate's rating for the job (0-5)", - optional: true, + _makeRequest({ + $ = this, path, ...opts + }) { + const config = { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }; + console.log("config: ", config); + return axios($, config); }, - companyId: { - type: "number", - label: "Company ID", - description: "The company ID related to the contact", + listCandidates(opts = {}) { + return this._makeRequest({ + path: "/candidates", + ...opts, + }); }, - emails: { - type: "string[]", - label: "Emails", - description: "An array of email objects", - optional: true, + listCompanies(opts = {}) { + return this._makeRequest({ + path: "/companies", + ...opts, + }); }, - isHot: { - type: "boolean", - label: "Is Hot", - description: "Whether the contact is marked as hot", - optional: true, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); }, - customFields: { - type: "string[]", - label: "Custom Fields", - description: "An array of custom field objects", - optional: true, + listCustomFields(opts = {}) { + return this._makeRequest({ + path: "/contacts/custom_fields", + ...opts, + }); }, - }, - methods: { - _baseUrl() { - return "https://api.catsone.com/v3"; + listJobs(opts = {}) { + return this._makeRequest({ + path: "/jobs", + ...opts, + }); }, - async _makeRequest(opts = {}) { - const { - $ = this, method = "GET", path = "/", headers, ...otherOpts - } = opts; - return axios($, { - ...otherOpts, - method, - url: this._baseUrl() + path, - headers: { - ...headers, - Authorization: `Bearer ${this.$auth.api_key}`, - }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, }); }, - async createCandidate(data) { - const result = await this._makeRequest({ + createCandidate(opts = {}) { + return this._makeRequest({ method: "POST", path: "/candidates", - data, + ...opts, }); - this.$emit(result, { - summary: "Candidate created", - name: "candidate.created", - }); - return result; }, - async addCandidateToJobPipeline(data) { + addCandidateToJobPipeline(opts = {}) { return this._makeRequest({ method: "POST", path: "/pipelines", - data, + ...opts, }); }, - async createContact(data) { - const result = await this._makeRequest({ + createContact(opts = {}) { + return this._makeRequest({ method: "POST", path: "/contacts", - data, - }); - this.$emit(result, { - summary: "Contact created", - name: "contact.created", + ...opts, }); - return result; }, - async createActivity(data) { - const result = await this._makeRequest({ + createWebhook(opts = {}) { + return this._makeRequest({ method: "POST", - path: "/activities", - data, + path: "/webhooks", + ...opts, }); - this.$emit(result, { - summary: "Activity created", - name: "activity.created", + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, }); - return result; }, }, - version: "0.0.{{ts}}", }; diff --git a/components/cats/common/utils.mjs b/components/cats/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/cats/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/cats/package.json b/components/cats/package.json index efdc957605db6..52701eb2cf6a7 100644 --- a/components/cats/package.json +++ b/components/cats/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/cats", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream CATS Components", "main": "cats.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "crypto": "^1.0.1" } } diff --git a/components/cats/sources/common/base.mjs b/components/cats/sources/common/base.mjs new file mode 100644 index 0000000000000..76c33f18ae210 --- /dev/null +++ b/components/cats/sources/common/base.mjs @@ -0,0 +1,80 @@ +import { + createHmac, randomUUID, +} from "crypto"; +import cats from "../../cats.app.mjs"; + +export default { + props: { + cats, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + }, + methods: { + _getUUID() { + return this.db.get("UUID"); + }, + _setUUID(UUID) { + this.db.set("UUID", UUID); + }, + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getExtraData() { + return {}; + }, + checkSignature({ + bodyRaw, headers, + }) { + const uuid = this._getUUID(); + const hash = createHmac("sha256", uuid).update(`${bodyRaw}${headers["x-request-id"]}`) + .digest() + .toString("hex"); + + console.log("hash: ", hash); + + return headers["x-signature"] === `HMAC-SHA256 ${hash}`; + }, + }, + hooks: { + async activate() { + const uuid = randomUUID(); + const { headers } = await this.cats.createWebhook({ + returnFullResponse: true, + data: { + target_url: this.http.endpoint, + events: this.getEventType(), + secret: uuid, + }, + }); + + const location = headers.location.split("/"); + const webhookId = location[location.length - 1]; + + this._setUUID(uuid); + this._setHookId(webhookId); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.cats.deleteWebhook(webhookId); + }, + }, + async run({ + body, ...event + }) { + if (!this.checkSignature(event)) { + return this.http.respond({ + status: 400, + }); + } + + console.log("this.generateMeta(body): ", this.generateMeta(body)); + + this.$emit(body, this.generateMeta(body)); + }, +}; diff --git a/components/cats/sources/new-activity-instant/new-activity-instant.mjs b/components/cats/sources/new-activity-instant/new-activity-instant.mjs index 8589524d44cee..0b4d4d010dd5a 100644 --- a/components/cats/sources/new-activity-instant/new-activity-instant.mjs +++ b/components/cats/sources/new-activity-instant/new-activity-instant.mjs @@ -1,99 +1,28 @@ -import cats from "../../cats.app.mjs"; -import crypto from "crypto"; -import { axios } from "@pipedream/platform"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, key: "cats-new-activity-instant", - name: "New Activity Instant", - description: "Emit a new event when an activity related to a cat is created. [See the documentation](https://docs.catsone.com/api/v3/)", - version: "0.0.{{ts}}", + name: "New Activity (Instant)", + description: "Emit new event when an activity related to a cat is created.", + version: "0.0.1", type: "source", dedupe: "unique", - props: { - cats, - http: { - type: "$.interface.http", - customResponse: true, - }, - db: "$.service.db", - }, methods: { - _getWebhookId() { - return this.db.get("webhookId"); - }, - _setWebhookId(id) { - this.db.set("webhookId", id); - }, - }, - hooks: { - async deploy() { - const activities = await this.cats._makeRequest({ - path: "/activities", - params: { - limit: 50, - order_by: "created_at.desc", - }, - }); - for (const activity of activities) { - this.$emit(activity, { - id: activity.id, - summary: `New activity: ${activity.type}`, - ts: Date.parse(activity.created_at), - }); - } + ...common.methods, + getEventType() { + return [ + "activity.created", + ]; }, - async activate() { - const webhookConfig = { - events: [ - "activity.created", - ], - target_url: this.http.endpoint, - secret: process.env.CATS_WEBHOOK_SECRET, // Assuming secret is stored as environment variable + generateMeta(body) { + return { + id: body.activity_id, + summary: `New activity: ${body.activity_id}`, + ts: Date.parse(body.date || new Date()), }; - const webhookResponse = await this.cats._makeRequest({ - method: "POST", - path: "/webhooks", - data: webhookConfig, - }); - this._setWebhookId(webhookResponse.id); }, - async deactivate() { - const webhookId = this._getWebhookId(); - if (webhookId) { - await this.cats._makeRequest({ - method: "DELETE", - path: `/webhooks/${webhookId}`, - }); - this.db.set("webhookId", null); - } - }, - }, - async run(event) { - const secret = process.env.CATS_WEBHOOK_SECRET; // Assuming secret is stored as environment variable - const signature = event.headers["x-signature"]; - const requestId = event.headers["x-request-id"]; - const payload = JSON.stringify(event.body) + requestId; - const computedSignature = `HMAC-SHA256 ${crypto - .createHmac("sha256", secret) - .update(payload) - .digest("hex")}`; - - if (computedSignature !== signature) { - this.http.respond({ - status: 401, - body: "Unauthorized", - }); - return; - } - - this.http.respond({ - status: 200, - }); - const activity = event.body; - this.$emit(activity, { - id: activity.id, - summary: `New activity: ${activity.type}`, - ts: Date.parse(activity.created_at), - }); }, + sampleEmit, }; diff --git a/components/cats/sources/new-activity-instant/test-event.mjs b/components/cats/sources/new-activity-instant/test-event.mjs new file mode 100644 index 0000000000000..f1fe0026dd622 --- /dev/null +++ b/components/cats/sources/new-activity-instant/test-event.mjs @@ -0,0 +1,41 @@ +export default { + "event": "activity.created", + "activity_id": 123456789, + "date": "2024-11-19T19:57:54+00:00", + "_links": { + "activity": { + "href": "/activities/123456789" + } + }, + "_embedded": { + "activity": { + "id": 123456789, + "data_item": { + "id": 123456789, + "type": "candidate" + }, + "date": "2024-11-19T19:57:54+00:00", + "regarding_id": 123456789, + "type": "other", + "notes": null, + "annotation": "Added candidate to pipeline: No Contact", + "entered_by_id": 123456789, + "date_created": "2024-11-19T19:57:54+00:00", + "date_modified": "2024-11-19T19:57:54+00:00", + "_links": { + "self": { + "href": "/activities/123456789" + }, + "regarding": { + "href": "/jobs/123456789" + }, + "data_item": { + "href": "/candidates/123456789" + }, + "entered_by": { + "href": "/users/123456789" + } + } + } + } +} \ No newline at end of file diff --git a/components/cats/sources/new-candidate-instant/new-candidate-instant.mjs b/components/cats/sources/new-candidate-instant/new-candidate-instant.mjs index 2ed742d64ecc6..5a2418396bbe0 100644 --- a/components/cats/sources/new-candidate-instant/new-candidate-instant.mjs +++ b/components/cats/sources/new-candidate-instant/new-candidate-instant.mjs @@ -1,90 +1,29 @@ -import cats from "../../cats.app.mjs"; -import crypto from "crypto"; -import { axios } from "@pipedream/platform"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, key: "cats-new-candidate-instant", - name: "New Candidate Instant", - description: "Emit a new event when a new candidate is created. [See the documentation](https://docs.catsone.com/api/v3/)", - version: "0.0.{{ts}}", + name: "New Candidate (Instant)", + description: "Emit new event when a new candidate is created.", + version: "0.0.1", type: "source", dedupe: "unique", - props: { - cats, - http: { - type: "$.interface.http", - customResponse: true, - }, - db: "$.service.db", - }, methods: { - _getWebhookId() { - return this.db.get("webhookId"); - }, - _setWebhookId(id) { - this.db.set("webhookId", id); + ...common.methods, + getEventType() { + return [ + "candidate.created", + ]; }, - }, - hooks: { - async deploy() { - // Fetching the most recent candidate events is not required as per the provided instructions - }, - async activate() { - const webhookConfig = { - events: [ - "candidate.created", - ], - target_url: this.http.endpoint, - secret: this.cats.$auth.api_key, // Using the API key as the secret for signature validation + generateMeta(body) { + const candidate = body._embedded.candidate; + return { + id: body.candidate_id, + summary: `New candidate created: ${candidate.first_name} ${candidate.last_name} (${candidate.emails.primary || candidate.emails.second})`, + ts: Date.parse(body.date || new Date()), }; - - const response = await axios(this, { - method: "POST", - url: `${this.cats._baseUrl()}/webhooks`, - headers: { - Authorization: `Bearer ${this.cats.$auth.api_key}`, - }, - data: webhookConfig, - }); - - const webhookId = response.id; - this._setWebhookId(webhookId); - }, - async deactivate() { - const webhookId = this._getWebhookId(); - if (webhookId) { - await axios(this, { - method: "DELETE", - url: `${this.cats._baseUrl()}/webhooks/${webhookId}`, - headers: { - Authorization: `Bearer ${this.cats.$auth.api_key}`, - }, - }); - } }, }, - async run(event) { - const signature = event.headers["X-Signature"]; - const rawBody = JSON.stringify(event.body); - const computedSignature = `HMAC-SHA256 ${crypto.createHmac("sha256", this.cats.$auth.api_key).update(rawBody) - .digest("hex")}`; - - if (computedSignature !== signature) { - this.http.respond({ - status: 401, - body: "Unauthorized", - }); - return; - } - - this.http.respond({ - status: 200, - body: "OK", - }); - this.$emit(event.body, { - id: event.body.id, - summary: `New candidate created: ${event.body.first_name} ${event.body.last_name}`, - ts: Date.now(), - }); - }, + sampleEmit, }; diff --git a/components/cats/sources/new-candidate-instant/test-event.mjs b/components/cats/sources/new-candidate-instant/test-event.mjs new file mode 100644 index 0000000000000..34e1779782171 --- /dev/null +++ b/components/cats/sources/new-candidate-instant/test-event.mjs @@ -0,0 +1,112 @@ +export default { + "event": "candidate.created", + "candidate_id": 123456789, + "date": "2024-11-19T20:13:12+00:00", + "_links": { + "candidate": { + "href": "/candidates/123456789" + } + }, + "_embedded": { + "candidate": { + "id": 123456789, + "first_name": "Candidate Name", + "middle_name": "Middle Name", + "last_name": "Last Name", + "title": "Candidate Tittle", + "emails": { + "primary": "candidate@email.com", + "secondary": null + }, + "address": { + "street": "street", + "city": "city", + "state": "CA", + "postal_code": "18234" + }, + "country_code": "US", + "social_media_urls": [], + "website": "https://website.com", + "phones": { + "home": null, + "cell": "1234567890", + "work": null + }, + "best_time_to_call": "14:00", + "current_employer": "CurrentEmployer", + "date_available": "2024-12-12", + "current_pay": "400", + "desired_pay": "8000", + "is_willing_to_relocate": true, + "key_skills": "", + "notes": "", + "is_hot": true, + "is_active": true, + "contact_id": null, + "owner_id": 123456, + "entered_by_id": 123456, + "source": "", + "is_registered": true, + "consent_status": null, + "date_created": "2024-11-19T20:13:12+00:00", + "date_modified": "2024-11-19T20:13:12+00:00", + "_links": { + "self": { + "href": "/candidates/123456789" + }, + "custom_fields": { + "href": "/candidates/123456789/custom_fields" + }, + "attachments": { + "href": "/candidates/123456789/attachments" + }, + "activities": { + "href": "/candidates/123456789/activities" + }, + "work_history": { + "href": "/candidates/123456789/work_history" + }, + "pipelines": { + "href": "/candidates/123456789/pipelines" + }, + "tags": { + "href": "/candidates/123456789/tags" + }, + "thumbnail": { + "href": "/candidates/123456789/thumbnail" + }, + "phones": { + "href": "/candidates/123456789/phones" + }, + "emails": { + "href": "/candidates/123456789/emails" + }, + "owner": { + "href": "/users/123456" + }, + "entered_by": { + "href": "/users/123456" + } + }, + "_embedded": { + "custom_fields": [], + "work_history": [], + "thumbnail": [ + { + "id": 92680, + "source": "gravatar", + "attachment_id": null, + "url": "https://pipedream.catsone.com/candidates/123456789/thumbnail?_s=e1221ae117fecea3c20ec9075dfb36ac05e876d1be10725e27b4eeb42289bd65", + "_links": { + "self": { + "href": "/candidates/123456789/thumbnail" + } + } + } + ], + "phones": [], + "emails": [] + } + } + } +} \ No newline at end of file diff --git a/components/cats/sources/new-contact-instant/new-contact-instant.mjs b/components/cats/sources/new-contact-instant/new-contact-instant.mjs index cd85a6d04c541..a3ae27151b3de 100644 --- a/components/cats/sources/new-contact-instant/new-contact-instant.mjs +++ b/components/cats/sources/new-contact-instant/new-contact-instant.mjs @@ -1,89 +1,29 @@ -import cats from "../../cats.app.mjs"; -import crypto from "crypto"; -import { axios } from "@pipedream/platform"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, key: "cats-new-contact-instant", - name: "New Contact Created", - description: "Emit a new event when a contact related to a cat is created. [See the documentation](https://docs.catsone.com/api/v3/)", - version: "0.0.{{ts}}", + name: "New Contact Created (Instant)", + description: "Emit new event when a contact related to a cat is created.", + version: "0.0.1", type: "source", dedupe: "unique", - props: { - cats: { - type: "app", - app: "cats", + methods: { + ...common.methods, + getEventType() { + return [ + "contact.created", + ]; }, - http: { - type: "$.interface.http", - customResponse: true, - }, - db: "$.service.db", - }, - hooks: { - async deploy() { - const contacts = await this.cats._makeRequest({ - path: "/contacts", - params: { - limit: 50, - orderBy: "created_at.desc", - }, - }); - for (const contact of contacts) { - this.$emit(contact, { - id: contact.id, - summary: `New contact: ${contact.firstName} ${contact.lastName}`, - ts: Date.parse(contact.createdAt), - }); - } - }, - async activate() { - const webhookData = { - events: [ - "contact.created", - ], - target_url: this.http.endpoint, - secret: this.db.get("secret"), + generateMeta(body) { + const contact = body._embedded.contact; + return { + id: body.contact_id, + summary: `New contact: ${contact.first_name} ${contact.last_name}`, + ts: Date.parse(body.date || new Date()), }; - const response = await this.cats._makeRequest({ - method: "POST", - path: "/webhooks", - data: webhookData, - }); - this.db.set("webhookId", response.id); - }, - async deactivate() { - const webhookId = this.db.get("webhookId"); - if (webhookId) { - await this.cats._makeRequest({ - method: "DELETE", - path: `/webhooks/${webhookId}`, - }); - } }, }, - async run(event) { - const webhookSignature = event.headers["x-signature"]; - const requestId = event.headers["x-request-id"]; - const secret = this.db.get("secret") || "your_secret_key"; - const rawBody = event.rawBody; - - const computedSignature = `HMAC-SHA256 ${crypto.createHmac("sha256", secret).update(rawBody + requestId) - .digest("hex")}`; - - if (computedSignature !== webhookSignature) { - this.http.respond({ - status: 401, - body: "Unauthorized", - }); - return; - } - - const contact = event.body; - this.$emit(contact, { - id: contact.id, - summary: `New contact: ${contact.firstName} ${contact.lastName}`, - ts: Date.parse(contact.createdAt), - }); - }, + sampleEmit, }; diff --git a/components/cats/sources/new-contact-instant/test-event.mjs b/components/cats/sources/new-contact-instant/test-event.mjs new file mode 100644 index 0000000000000..3fd9ac647f591 --- /dev/null +++ b/components/cats/sources/new-contact-instant/test-event.mjs @@ -0,0 +1,103 @@ +export default { + "event": "contact.created", + "contact_id": 123456789, + "date": "2024-11-19T20:21:10+00:00", + "_links": { + "contact": { + "href": "/contacts/123456789" + } + }, + "_embedded": { + "contact": { + "id": "123456789", + "first_name": "Contact Name", + "last_name": "Last Name", + "title": "Contact title", + "reports_to_id": 1234567, + "owner_id": 123456, + "company_id": 22978541, + "emails": { + "primary": "contact@email.com", + "secondary": null + }, + "phones": { + "work": null, + "cell": "12345678", + "other": null + }, + "address": { + "street": "street", + "city": "city", + "state": "CA", + "postal_code": "92132" + }, + "country_code": "US", + "social_media_urls": [], + "is_hot": false, + "has_left_company": false, + "notes": "", + "entered_by_id": 123456, + "consent_status": null, + "date_created": "2024-11-19T20:21:10+00:00", + "date_modified": "2024-11-19T20:21:10+00:00", + "status_id": 123456, + "_links": { + "self": { + "href": "/contacts/123456789" + }, + "reports_to": { + "href": "/users/1234567" + }, + "custom_fields": { + "href": "/contacts/123456789/custom_fields" + }, + "activities": { + "href": "/contacts/123456789/activities" + }, + "status": { + "href": "/contacts/statuses/123456" + }, + "entered_by": { + "href": "/users/123456" + }, + "owner": { + "href": "/users/123456" + }, + "attachments": { + "href": "/contacts/123456789/attachments" + }, + "tags": { + "href": "/contacts/123456789/tags" + }, + "thumbnail": { + "href": "/contacts/123456789/thumbnail" + }, + "phones": { + "href": "/contacts/123456789/phones" + }, + "emails": { + "href": "/contacts/123456789/emails" + } + }, + "_embedded": { + "custom_fields": [], + "status": { + "id": 123456, + "workflow_id": 5696671, + "title": "No Status", + "mapping": "", + "prerequisites": [], + "triggers": [], + "_links": { + "self": { + "href": "contacts/statuses/123456" + } + } + }, + "thumbnail": [], + "phones": [], + "emails": [] + } + } + } +} \ No newline at end of file From ba14767d1c27cc759f8a4ce06845d3fa3560b486 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Tue, 19 Nov 2024 17:25:45 -0300 Subject: [PATCH 3/3] pnpm update --- pnpm-lock.yaml | 109 ++++++++++++++++++++++++++----------------------- 1 file changed, 57 insertions(+), 52 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1bb40f3699cb3..5cb6e8380fbe0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1545,7 +1545,12 @@ importers: '@pipedream/platform': 1.6.0 components/cats: - specifiers: {} + specifiers: + '@pipedream/platform': ^3.0.3 + crypto: ^1.0.1 + dependencies: + '@pipedream/platform': 3.0.3 + crypto: 1.0.1 components/cdc_national_environmental_public_health_tracking: specifiers: {} @@ -13414,55 +13419,6 @@ packages: - aws-crt dev: false - /@aws-sdk/client-sso-oidc/3.600.0_tdq3komn4zwyd65w7klbptsu34: - resolution: {integrity: sha512-7+I8RWURGfzvChyNQSyj5/tKrqRbzRl7H+BnTOf/4Vsw1nFOi5ROhlhD4X/Y0QCTacxnaoNcIrqnY7uGGvVRzw==} - engines: {node: '>=16.0.0'} - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sts': 3.600.0 - '@aws-sdk/core': 3.598.0 - '@aws-sdk/credential-provider-node': 3.600.0_f7n47caigsrjd2lr2szmwfuee4 - '@aws-sdk/middleware-host-header': 3.598.0 - '@aws-sdk/middleware-logger': 3.598.0 - '@aws-sdk/middleware-recursion-detection': 3.598.0 - '@aws-sdk/middleware-user-agent': 3.598.0 - '@aws-sdk/region-config-resolver': 3.598.0 - '@aws-sdk/types': 3.598.0 - '@aws-sdk/util-endpoints': 3.598.0 - '@aws-sdk/util-user-agent-browser': 3.598.0 - '@aws-sdk/util-user-agent-node': 3.598.0 - '@smithy/config-resolver': 3.0.3 - '@smithy/core': 2.2.3 - '@smithy/fetch-http-handler': 3.2.1 - '@smithy/hash-node': 3.0.2 - '@smithy/invalid-dependency': 3.0.2 - '@smithy/middleware-content-length': 3.0.2 - '@smithy/middleware-endpoint': 3.0.4 - '@smithy/middleware-retry': 3.0.6 - '@smithy/middleware-serde': 3.0.3 - '@smithy/middleware-stack': 3.0.3 - '@smithy/node-config-provider': 3.1.3 - '@smithy/node-http-handler': 3.1.2 - '@smithy/protocol-http': 4.0.3 - '@smithy/smithy-client': 3.1.6 - '@smithy/types': 3.3.0 - '@smithy/url-parser': 3.0.3 - '@smithy/util-base64': 3.0.0 - '@smithy/util-body-length-browser': 3.0.0 - '@smithy/util-body-length-node': 3.0.0 - '@smithy/util-defaults-mode-browser': 3.0.6 - '@smithy/util-defaults-mode-node': 3.0.6 - '@smithy/util-endpoints': 2.0.3 - '@smithy/util-middleware': 3.0.3 - '@smithy/util-retry': 3.0.2 - '@smithy/util-utf8': 3.0.0 - tslib: 2.6.3 - transitivePeerDependencies: - - '@aws-sdk/client-sts' - - aws-crt - dev: false - /@aws-sdk/client-sso/3.423.0: resolution: {integrity: sha512-znIufHkwhCIePgaYciIs3x/+BpzR57CZzbCKHR9+oOvGyufEPPpUT5bFLvbwTgfiVkTjuk6sG/ES3U5Bc+xtrA==} engines: {node: '>=14.0.0'} @@ -13698,7 +13654,55 @@ packages: dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sso-oidc': 3.600.0_tdq3komn4zwyd65w7klbptsu34 + '@aws-sdk/client-sso-oidc': 3.600.0 + '@aws-sdk/core': 3.598.0 + '@aws-sdk/credential-provider-node': 3.600.0_f7n47caigsrjd2lr2szmwfuee4 + '@aws-sdk/middleware-host-header': 3.598.0 + '@aws-sdk/middleware-logger': 3.598.0 + '@aws-sdk/middleware-recursion-detection': 3.598.0 + '@aws-sdk/middleware-user-agent': 3.598.0 + '@aws-sdk/region-config-resolver': 3.598.0 + '@aws-sdk/types': 3.598.0 + '@aws-sdk/util-endpoints': 3.598.0 + '@aws-sdk/util-user-agent-browser': 3.598.0 + '@aws-sdk/util-user-agent-node': 3.598.0 + '@smithy/config-resolver': 3.0.3 + '@smithy/core': 2.2.3 + '@smithy/fetch-http-handler': 3.2.1 + '@smithy/hash-node': 3.0.2 + '@smithy/invalid-dependency': 3.0.2 + '@smithy/middleware-content-length': 3.0.2 + '@smithy/middleware-endpoint': 3.0.4 + '@smithy/middleware-retry': 3.0.6 + '@smithy/middleware-serde': 3.0.3 + '@smithy/middleware-stack': 3.0.3 + '@smithy/node-config-provider': 3.1.3 + '@smithy/node-http-handler': 3.1.2 + '@smithy/protocol-http': 4.0.3 + '@smithy/smithy-client': 3.1.6 + '@smithy/types': 3.3.0 + '@smithy/url-parser': 3.0.3 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.6 + '@smithy/util-defaults-mode-node': 3.0.6 + '@smithy/util-endpoints': 2.0.3 + '@smithy/util-middleware': 3.0.3 + '@smithy/util-retry': 3.0.2 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sts/3.600.0_dseaa2p5u2yk67qiepewcq3hkq: + resolution: {integrity: sha512-KQG97B7LvTtTiGmjlrG1LRAY8wUvCQzrmZVV5bjrJ/1oXAU7DITYwVbSJeX9NWg6hDuSk0VE3MFwIXS2SvfLIA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.600.0 '@aws-sdk/core': 3.598.0 '@aws-sdk/credential-provider-node': 3.600.0_f7n47caigsrjd2lr2szmwfuee4 '@aws-sdk/middleware-host-header': 3.598.0 @@ -13737,6 +13741,7 @@ packages: '@smithy/util-utf8': 3.0.0 tslib: 2.6.3 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt dev: false @@ -18083,7 +18088,7 @@ packages: '@aws-sdk/client-sns': 3.423.0 '@aws-sdk/client-sqs': 3.423.0 '@aws-sdk/client-ssm': 3.423.0 - '@aws-sdk/client-sts': 3.600.0 + '@aws-sdk/client-sts': 3.600.0_dseaa2p5u2yk67qiepewcq3hkq '@aws-sdk/s3-request-presigner': 3.609.0 '@pipedream/helper_functions': 0.3.12 '@pipedream/platform': 1.6.6