From ceca95f7f4ff81475f00c6d861453c66778951c5 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Thu, 27 Feb 2025 13:08:26 -0300 Subject: [PATCH 1/5] gem init --- .../create-candidate/create-candidate.mjs | 67 ++++ .../update-candidate-stage.mjs | 44 +++ components/gem/gem.app.mjs | 285 +++++++++++++++++- components/gem/package.json | 2 +- .../sources/new-candidate/new-candidate.mjs | 142 +++++++++ .../new-stage-change/new-stage-change.mjs | 105 +++++++ 6 files changed, 641 insertions(+), 4 deletions(-) create mode 100644 components/gem/actions/create-candidate/create-candidate.mjs create mode 100644 components/gem/actions/update-candidate-stage/update-candidate-stage.mjs create mode 100644 components/gem/sources/new-candidate/new-candidate.mjs create mode 100644 components/gem/sources/new-stage-change/new-stage-change.mjs diff --git a/components/gem/actions/create-candidate/create-candidate.mjs b/components/gem/actions/create-candidate/create-candidate.mjs new file mode 100644 index 0000000000000..88c757433c722 --- /dev/null +++ b/components/gem/actions/create-candidate/create-candidate.mjs @@ -0,0 +1,67 @@ +import gem from "../../gem.app.mjs"; +import { axios } from "@pipedream/platform"; + +export default { + key: "gem-create-candidate", + name: "Create Candidate", + description: "Creates a new candidate in Gem. [See the documentation]()", + version: "0.0.{{ts}}", + type: "action", + props: { + gem, + firstName: { + propDefinition: [ + gem, + "firstName", + ], + }, + lastName: { + propDefinition: [ + gem, + "lastName", + ], + }, + emails: { + propDefinition: [ + gem, + "emails", + ], + }, + jobPosition: { + propDefinition: [ + gem, + "jobPosition", + ], + optional: true, + }, + source: { + propDefinition: [ + gem, + "source", + ], + optional: true, + }, + notes: { + propDefinition: [ + gem, + "notes", + ], + optional: true, + }, + }, + async run({ $ }) { + const candidate = await this.gem.createCandidate({ + firstName: this.firstName, + lastName: this.lastName, + emails: this.emails, + jobPosition: this.jobPosition, + source: this.source, + notes: this.notes, + }); + $.export( + "$summary", + `Created candidate ${candidate.first_name} ${candidate.last_name} with ID: ${candidate.id}`, + ); + return candidate; + }, +}; diff --git a/components/gem/actions/update-candidate-stage/update-candidate-stage.mjs b/components/gem/actions/update-candidate-stage/update-candidate-stage.mjs new file mode 100644 index 0000000000000..1f52a31d30ea1 --- /dev/null +++ b/components/gem/actions/update-candidate-stage/update-candidate-stage.mjs @@ -0,0 +1,44 @@ +import gem from "../../gem.app.mjs"; +import { axios } from "@pipedream/platform"; + +export default { + key: "gem-update-candidate-stage", + name: "Update Candidate Stage", + description: "Updates the hiring stage of an existing candidate. [See the documentation]()", + version: "0.0.{{ts}}", + type: "action", + props: { + gem, + candidateId: { + propDefinition: [ + "gem", + "candidateId", + ], + }, + newStage: { + propDefinition: [ + "gem", + "newStage", + ], + }, + changeNote: { + propDefinition: [ + "gem", + "changeNote", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.gem.updateCandidateStage({ + candidateId: this.candidateId, + newStage: this.newStage, + changeNote: this.changeNote, + }); + $.export( + "$summary", + `Updated candidate stage for candidate ${this.candidateId} to new stage ${this.newStage}`, + ); + return response; + }, +}; diff --git a/components/gem/gem.app.mjs b/components/gem/gem.app.mjs index c48916e8ad5ef..d6bfcddd9bcdb 100644 --- a/components/gem/gem.app.mjs +++ b/components/gem/gem.app.mjs @@ -1,11 +1,290 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "gem", - propDefinitions: {}, + version: "0.0.{{ts}}", + propDefinitions: { + // Emit New Candidate Event Props + filterJobPositions: { + type: "string[]", + label: "Job Positions Filter", + description: "Filter events by specific job positions", + optional: true, + async options() { + const positions = await this.listJobPositions(); + return positions.map((position) => ({ + label: position.name, + value: position.id, + })); + }, + }, + filterRecruiters: { + type: "string[]", + label: "Recruiters Filter", + description: "Filter events by specific recruiters", + optional: true, + async options() { + const recruiters = await this.listRecruiters(); + return recruiters.map((recruiter) => ({ + label: recruiter.name, + value: recruiter.id, + })); + }, + }, + + // Emit Candidate Stage Change Event Props + monitorPipelines: { + type: "string[]", + label: "Pipelines to Monitor", + description: "Specify which pipelines to monitor for stage changes", + optional: true, + async options() { + const pipelines = await this.listPipelines(); + return pipelines.map((pipeline) => ({ + label: pipeline.name, + value: pipeline.id, + })); + }, + }, + monitorStages: { + type: "string[]", + label: "Stages to Monitor", + description: "Specify which stages to monitor for stage changes", + optional: true, + async options({ monitorPipelines }) { + if (!monitorPipelines || monitorPipelines.length === 0) return []; + const stages = await this.listStages({ + pipelineIds: monitorPipelines, + }); + return stages.map((stage) => ({ + label: stage.name, + value: stage.id, + })); + }, + }, + + // Create Candidate Props + firstName: { + type: "string", + label: "First Name", + description: "Candidate's first name", + }, + lastName: { + type: "string", + label: "Last Name", + description: "Candidate's last name", + }, + emails: { + type: "string[]", + label: "Email Addresses", + description: "List of candidate's email addresses", + }, + jobPosition: { + type: "string", + label: "Job Position", + description: "The position the candidate is applying for", + optional: true, + async options() { + const positions = await this.listJobPositions(); + return positions.map((position) => ({ + label: position.name, + value: position.id, + })); + }, + }, + source: { + type: "string", + label: "Source", + description: "Source of the candidate", + optional: true, + }, + notes: { + type: "string", + label: "Notes", + description: "Additional notes about the candidate", + optional: true, + }, + + // Update Candidate Stage Props + candidateId: { + type: "string", + label: "Candidate ID", + description: "The ID of the candidate to update", + }, + newStage: { + type: "string", + label: "New Stage", + description: "The new stage for the candidate", + async options({ monitorPipelines }) { + if (!monitorPipelines || monitorPipelines.length === 0) return []; + const stages = await this.listStages({ + pipelineIds: monitorPipelines, + }); + return stages.map((stage) => ({ + label: stage.name, + value: stage.id, + })); + }, + }, + changeNote: { + type: "string", + label: "Change Note", + description: "Optional note or reason for stage change", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data + // Log Authentication Keys authKeys() { console.log(Object.keys(this.$auth)); }, + + // Base URL Method + _baseUrl() { + return "https://api.gem.com/v0"; + }, + + // Make Request Method + async _makeRequest(opts = {}) { + const { + $ = this, + method = "GET", + path = "/", + headers, + ...otherOpts + } = opts; + return axios($, { + method, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + ...otherOpts, + }); + }, + + // Pagination Method + async paginate(fn, ...opts) { + const results = []; + let page = 1; + let more = true; + while (more) { + const response = await fn({ + page, + ...opts, + }); + if (!response || response.length === 0) { + more = false; + } else { + results.push(...response); + page += 1; + } + } + return results; + }, + + // List Job Positions + async listJobPositions(opts = {}) { + return this.paginate(this.listJobPositionsPage, opts); + }, + async listJobPositionsPage({ + page, ...opts + }) { + return this._makeRequest({ + method: "GET", + path: `/job_positions?page=${page}`, + ...opts, + }); + }, + + // List Recruiters + async listRecruiters(opts = {}) { + return this.paginate(this.listRecruitersPage, opts); + }, + async listRecruitersPage({ + page, ...opts + }) { + return this._makeRequest({ + method: "GET", + path: `/recruiters?page=${page}`, + ...opts, + }); + }, + + // List Pipelines + async listPipelines(opts = {}) { + return this.paginate(this.listPipelinesPage, opts); + }, + async listPipelinesPage({ + page, ...opts + }) { + return this._makeRequest({ + method: "GET", + path: `/pipelines?page=${page}`, + ...opts, + }); + }, + + // List Stages + async listStages({ + pipelineIds = [], ...opts + }) { + if (pipelineIds.length === 0) return []; + const stagePromises = pipelineIds.map((pipelineId) => + this.paginate(this.listStagesPage, { + pipelineId, + ...opts, + })); + const stagesArrays = await Promise.all(stagePromises); + return stagesArrays.flat(); + }, + async listStagesPage({ + pipelineId, page, ...opts + }) { + return this._makeRequest({ + method: "GET", + path: `/pipelines/${pipelineId}/stages?page=${page}`, + ...opts, + }); + }, + + // Create a New Candidate + async createCandidate({ + firstName, lastName, emails, jobPosition, source, notes, + }) { + const data = { + first_name: firstName, + last_name: lastName, + emails: emails.map((email) => ({ + email_address: email, + is_primary: false, + })), + }; + if (jobPosition) data.title = jobPosition; + if (source) data.sourced_from = source; + if (notes) data.notes = notes; + return this._makeRequest({ + method: "POST", + path: "/candidates", + data, + }); + }, + + // Update Candidate Stage + async updateCandidateStage({ + candidateId, newStage, changeNote, + }) { + const data = { + stage: newStage, + }; + if (changeNote) data.notes = changeNote; + return this._makeRequest({ + method: "PUT", + path: `/candidates/${candidateId}`, + data, + }); + }, }, -}; \ No newline at end of file +}; diff --git a/components/gem/package.json b/components/gem/package.json index 7a97537822dd4..00acd25ff41d6 100644 --- a/components/gem/package.json +++ b/components/gem/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/gem/sources/new-candidate/new-candidate.mjs b/components/gem/sources/new-candidate/new-candidate.mjs new file mode 100644 index 0000000000000..dabea1e9b04f1 --- /dev/null +++ b/components/gem/sources/new-candidate/new-candidate.mjs @@ -0,0 +1,142 @@ +import { + axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import gem from "../../gem.app.mjs"; + +export default { + key: "gem-new-candidate", + name: "New Candidate Added", + description: "Emit a new event when a candidate is added in Gem. [See the documentation]()", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + gem: { + type: "app", + app: "gem", + }, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + filterJobPositions: { + propDefinition: [ + "gem", + "filterJobPositions", + ], + }, + filterRecruiters: { + propDefinition: [ + "gem", + "filterRecruiters", + ], + }, + }, + hooks: { + async deploy() { + const pageSize = 50; + let page = 1; + let candidates = []; + let more = true; + + while (more && candidates.length < pageSize) { + const fetchedCandidates = await this.gem.listCandidates({ + page, + perPage: pageSize, + }); + if (fetchedCandidates.length === 0) { + more = false; + } else { + candidates.push(...fetchedCandidates); + if (fetchedCandidates.length < pageSize) { + more = false; + } else { + page += 1; + } + } + } + + const recentCandidates = candidates.slice(0, pageSize); + for (const candidate of recentCandidates) { + this.$emit( + candidate, + { + id: candidate.id, + summary: `New Candidate: ${candidate.first_name} ${candidate.last_name}`, + ts: new Date(candidate.created_at).getTime(), + }, + ); + } + + if (recentCandidates.length > 0) { + const latestTimestamp = new Date(recentCandidates[0].created_at).getTime(); + await this.db.set("lastTimestamp", latestTimestamp); + } + }, + async activate() { + // No webhook subscription needed for polling source + }, + async deactivate() { + // No webhook subscription to remove for polling source + }, + }, + async run() { + const lastTimestamp = (await this.db.get("lastTimestamp")) || 0; + let page = 1; + const newCandidates = []; + let more = true; + const pageSize = 50; + + while (more && newCandidates.length < pageSize) { + const fetchedCandidates = await this.gem.listCandidates({ + page, + perPage: pageSize, + createdAfter: lastTimestamp, + job_position_ids: this.filterJobPositions || [], + recruiter_ids: this.filterRecruiters || [], + }); + if (fetchedCandidates.length === 0) { + more = false; + } else { + for (const candidate of fetchedCandidates) { + const candidateTimestamp = new Date(candidate.created_at).getTime(); + if (candidateTimestamp > lastTimestamp) { + newCandidates.push(candidate); + if (newCandidates.length >= pageSize) { + break; + } + } + } + if (fetchedCandidates.length < pageSize) { + more = false; + } else { + page += 1; + } + } + } + + for (const candidate of newCandidates) { + this.$emit( + candidate, + { + id: candidate.id, + summary: `New Candidate: ${candidate.first_name} ${candidate.last_name}`, + ts: new Date(candidate.created_at).getTime(), + }, + ); + } + + if (newCandidates.length > 0) { + const latestTimestamp = newCandidates.reduce((max, candidate) => { + const candidateTime = new Date(candidate.created_at).getTime(); + return candidateTime > max + ? candidateTime + : max; + }, lastTimestamp); + await this.db.set("lastTimestamp", latestTimestamp); + } + }, +}; diff --git a/components/gem/sources/new-stage-change/new-stage-change.mjs b/components/gem/sources/new-stage-change/new-stage-change.mjs new file mode 100644 index 0000000000000..81607294eb68d --- /dev/null +++ b/components/gem/sources/new-stage-change/new-stage-change.mjs @@ -0,0 +1,105 @@ +import { + axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import gem from "../../gem.app.mjs"; + +export default { + key: "gem-new-stage-change", + name: "New Stage Change", + description: "Emit a new event when a candidate's stage changes in a hiring pipeline. [See the documentation]()", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + gem: { + type: "app", + app: "gem", + }, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + monitorPipelines: { + propDefinition: [ + "gem", + "monitorPipelines", + ], + }, + monitorStages: { + propDefinition: [ + "gem", + "monitorStages", + (c) => ({ + monitorPipelines: c.monitorPipelines, + }), + ], + }, + }, + hooks: { + async deploy() { + const candidates = await this.gem.listCandidates({ + monitorPipelines: this.monitorPipelines, + }); + + const stageData = {}; + + const sortedCandidates = candidates.sort((a, b) => new Date(b.last_updated_at) - new Date(a.last_updated_at)); + + const recentCandidates = sortedCandidates.slice(0, 50); + + for (const candidate of recentCandidates) { + this.$emit( + candidate, + { + id: candidate.id, + summary: `Candidate ${candidate.firstName} ${candidate.lastName} moved to stage ${candidate.stage}.`, + ts: Date.parse(candidate.last_updated_at) || Date.now(), + }, + ); + stageData[candidate.id] = candidate.stage; + } + + await this.db.set("candidates", stageData); + }, + async activate() { + // No webhook setup required for polling source + }, + async deactivate() { + // No webhook teardown required for polling source + }, + }, + async run() { + const lastStageData = (await this.db.get("candidates")) || {}; + const candidates = await this.gem.listCandidates({ + monitorPipelines: this.monitorPipelines, + }); + + const newStageData = {}; + + for (const candidate of candidates) { + newStageData[candidate.id] = candidate.stage; + + const previousStage = lastStageData[candidate.id]; + if (previousStage && previousStage !== candidate.stage) { + if ( + this.monitorStages.length === 0 || + this.monitorStages.includes(candidate.stage) + ) { + this.$emit( + candidate, + { + id: candidate.id, + summary: `Candidate ${candidate.firstName} ${candidate.lastName} moved to stage ${candidate.stage}.`, + ts: Date.parse(candidate.last_updated_at) || Date.now(), + }, + ); + } + } + } + + await this.db.set("candidates", newStageData); + }, +}; From d9002fbf301b4dc51ec261647090a00d63b48c77 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Fri, 28 Feb 2025 14:59:42 -0300 Subject: [PATCH 2/5] [Components] gem #15752 Sources - New Candidate Actions - Create Candidate --- .../create-candidate/create-candidate.mjs | 172 ++++++++-- .../update-candidate-stage.mjs | 44 --- components/gem/common/constants.mjs | 9 + components/gem/gem.app.mjs | 316 ++++-------------- components/gem/package.json | 5 +- .../sources/new-candidate/new-candidate.mjs | 153 +++------ .../gem/sources/new-candidate/test-event.mjs | 77 +++++ .../new-stage-change/new-stage-change.mjs | 105 ------ 8 files changed, 341 insertions(+), 540 deletions(-) delete mode 100644 components/gem/actions/update-candidate-stage/update-candidate-stage.mjs create mode 100644 components/gem/common/constants.mjs create mode 100644 components/gem/sources/new-candidate/test-event.mjs delete mode 100644 components/gem/sources/new-stage-change/new-stage-change.mjs diff --git a/components/gem/actions/create-candidate/create-candidate.mjs b/components/gem/actions/create-candidate/create-candidate.mjs index 88c757433c722..f51744e8d065d 100644 --- a/components/gem/actions/create-candidate/create-candidate.mjs +++ b/components/gem/actions/create-candidate/create-candidate.mjs @@ -1,66 +1,170 @@ +import { SOURCED_FROM } from "../../common/constants.mjs"; import gem from "../../gem.app.mjs"; -import { axios } from "@pipedream/platform"; export default { key: "gem-create-candidate", name: "Create Candidate", - description: "Creates a new candidate in Gem. [See the documentation]()", - version: "0.0.{{ts}}", + description: "Creates a new candidate in Gem. [See the documentation](https://api.gem.com/v0/reference#tag/Candidates/paths/~1v0~1candidates/post)", + version: "0.0.1", type: "action", props: { gem, - firstName: { + createdBy: { propDefinition: [ gem, - "firstName", + "createdBy", ], }, + firstName: { + type: "string", + label: "First Name", + description: "Candidate's first name", + optional: true, + }, lastName: { - propDefinition: [ - gem, - "lastName", - ], + type: "string", + label: "Last Name", + description: "Candidate's last name", + optional: true, }, - emails: { - propDefinition: [ - gem, - "emails", - ], + nickname: { + type: "string", + label: "Nickname", + description: "Candidate's nickname", + optional: true, }, - jobPosition: { - propDefinition: [ - gem, - "jobPosition", - ], + primaryEmail: { + type: "string", + label: "Primary Email Address", + description: "Candidate's primary email address", + }, + additionalEmails: { + type: "string[]", + label: "Email Addresses", + description: "List of candidate's additional email addresses", optional: true, }, - source: { - propDefinition: [ - gem, - "source", - ], + linkedInHandle: { + type: "string", + label: "LinkedIn Handle", + description: "If `LinkedIn Handle` is provided, candidate creation will be de-duplicated. If a candidate with the provided `LinkedIn Handle already exists, a 400 error will be returned with `errors` containing information on the existing candidate in this shape: `{\"errors\": { \"duplicate_candidate\": { \"id\": \"string\", \"linked_in_handle\": \"string\" }}}`.", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "Candidate's job title", optional: true, }, - notes: { + company: { + type: "string", + label: "Company", + description: "Candidate's company name", + optional: true, + }, + location: { + type: "string", + label: "Location", + description: "Candidate's location", + optional: true, + }, + school: { + type: "string", + label: "School", + description: "Candidate's school", + optional: true, + }, + educationalInfo: { + type: "string[]", + label: "Educational Info", + description: "A list of objects containing candidate's educational information. **Format: [{\"school\": \"string\", \"parsed_university\": \"string\", \"parsed_school\": \"string\", \"start_date\": \"string\", \"end_date\": \"string\", \"field_of_study\": \"string\", \"parsed_major_1\": \"string\", \"parsed_major_2\": \"string\", \"degree\": \"string\"}]**. [See the documentation](https://api.gem.com/v0/reference#tag/Candidates/paths/~1v0~1candidates/post) for further details.", + optional: true, + }, + workInfo: { + type: "string", + label: "Work Info", + description: "A list of objects containing candidate's work information. **Format: [{\"company\": \"string\", \"title\": \"string\", \"work_start_date\": \"string\", \"work_end_date\": \"string\", \"is_current\": \"string\"}]**. [See the documentation](https://api.gem.com/v0/reference#tag/Candidates/paths/~1v0~1candidates/post) for further details.", + optional: true, + }, + profileUrls: { + type: "string[]", + label: "Profile URLs", + description: "If `Profile URLs` is provided with an array of urls, social `profiles` will be generated based on the provided urls and attached to the candidate", + optional: true, + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "Candidate's phone number", + optional: true, + }, + projectIds: { propDefinition: [ gem, - "notes", + "projectIds", ], optional: true, }, + customFields: { + type: "string[]", + label: "Custom Fields", + description: "Array of objects containing new custom field values. Only custom fields specified in the array are updated. **Format: [{\"custom_field_id\": \"string\", \"value\": \"string\"}]**. [See the documentation](https://api.gem.com/v0/reference#tag/Candidates/paths/~1v0~1candidates/post) for further details.", + optional: true, + }, + sourcedFrom: { + type: "string", + label: "Sourced From", + description: "Where the candidate was sourced from", + options: SOURCED_FROM, + optional: true, + }, + autofill: { + type: "boolean", + label: "Autofill", + description: "Requires `Linked In Handle` to be non-null. Attempts to fill in any missing fields.", + optional: true, + }, }, async run({ $ }) { + const emails = [ + { + email_address: this.primaryEmail, + is_primary: true, + }, + ]; + if (this.additionalEmails) emails.push(...this.additionalEmails.map((email) => ({ + email_address: email, + is_primary: false, + }))); + + if (emails.length === 0) { + throw new Error("Primary Email Address is required"); + } const candidate = await this.gem.createCandidate({ - firstName: this.firstName, - lastName: this.lastName, - emails: this.emails, - jobPosition: this.jobPosition, - source: this.source, - notes: this.notes, + $, + data: { + created_by: this.createdBy, + first_name: this.firstName, + last_name: this.lastName, + nickname: this.nickname, + emails, + linked_in_handle: this.linkedInHandle, + title: this.title, + company: this.company, + location: this.location, + school: this.school, + educational_info: this.educationalInfo, + work_info: this.workInfo, + profile_urls: this.profileUrls, + phone_number: this.phoneNumber, + project_ids: this.projectIds, + custom_fields: this.customFields, + sourced_from: this.sourcedFrom, + autofill: this.autofill, + }, }); $.export( - "$summary", - `Created candidate ${candidate.first_name} ${candidate.last_name} with ID: ${candidate.id}`, + "$summary", `Created candidate ${candidate.first_name} ${candidate.last_name} with ID: ${candidate.id}`, ); return candidate; }, diff --git a/components/gem/actions/update-candidate-stage/update-candidate-stage.mjs b/components/gem/actions/update-candidate-stage/update-candidate-stage.mjs deleted file mode 100644 index 1f52a31d30ea1..0000000000000 --- a/components/gem/actions/update-candidate-stage/update-candidate-stage.mjs +++ /dev/null @@ -1,44 +0,0 @@ -import gem from "../../gem.app.mjs"; -import { axios } from "@pipedream/platform"; - -export default { - key: "gem-update-candidate-stage", - name: "Update Candidate Stage", - description: "Updates the hiring stage of an existing candidate. [See the documentation]()", - version: "0.0.{{ts}}", - type: "action", - props: { - gem, - candidateId: { - propDefinition: [ - "gem", - "candidateId", - ], - }, - newStage: { - propDefinition: [ - "gem", - "newStage", - ], - }, - changeNote: { - propDefinition: [ - "gem", - "changeNote", - ], - optional: true, - }, - }, - async run({ $ }) { - const response = await this.gem.updateCandidateStage({ - candidateId: this.candidateId, - newStage: this.newStage, - changeNote: this.changeNote, - }); - $.export( - "$summary", - `Updated candidate stage for candidate ${this.candidateId} to new stage ${this.newStage}`, - ); - return response; - }, -}; diff --git a/components/gem/common/constants.mjs b/components/gem/common/constants.mjs new file mode 100644 index 0000000000000..7859117aa62b4 --- /dev/null +++ b/components/gem/common/constants.mjs @@ -0,0 +1,9 @@ +export const LIMIT = 100; + +export const SOURCED_FROM = [ + "SeekOut", + "hireEZ", + "Starcircle", + "Censia", + "Consider", +]; diff --git a/components/gem/gem.app.mjs b/components/gem/gem.app.mjs index d6bfcddd9bcdb..eb54c779df968 100644 --- a/components/gem/gem.app.mjs +++ b/components/gem/gem.app.mjs @@ -1,290 +1,120 @@ import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; export default { type: "app", app: "gem", - version: "0.0.{{ts}}", propDefinitions: { - // Emit New Candidate Event Props - filterJobPositions: { - type: "string[]", - label: "Job Positions Filter", - description: "Filter events by specific job positions", - optional: true, - async options() { - const positions = await this.listJobPositions(); - return positions.map((position) => ({ - label: position.name, - value: position.id, - })); - }, - }, - filterRecruiters: { - type: "string[]", - label: "Recruiters Filter", - description: "Filter events by specific recruiters", - optional: true, - async options() { - const recruiters = await this.listRecruiters(); - return recruiters.map((recruiter) => ({ - label: recruiter.name, - value: recruiter.id, - })); - }, - }, + createdBy: { + type: "string", + label: "Created By", + description: "Who the candidate was created by", + async options({ page }) { + const data = await this.listUsers({ + params: { + page: page + 1, + page_size: LIMIT, + }, + }); - // Emit Candidate Stage Change Event Props - monitorPipelines: { - type: "string[]", - label: "Pipelines to Monitor", - description: "Specify which pipelines to monitor for stage changes", - optional: true, - async options() { - const pipelines = await this.listPipelines(); - return pipelines.map((pipeline) => ({ - label: pipeline.name, - value: pipeline.id, + return data.map(({ + id: value, email: label, + }) => ({ + label, + value, })); }, }, - monitorStages: { + projectIds: { type: "string[]", - label: "Stages to Monitor", - description: "Specify which stages to monitor for stage changes", - optional: true, - async options({ monitorPipelines }) { - if (!monitorPipelines || monitorPipelines.length === 0) return []; - const stages = await this.listStages({ - pipelineIds: monitorPipelines, + label: "Project Ids", + description: "If `Project Ids` is provided with an array of project ids, the candidate will be added into the projects once they are created.", + async options({ page }) { + const data = await this.listProjects({ + params: { + page: page + 1, + page_size: LIMIT, + }, }); - return stages.map((stage) => ({ - label: stage.name, - value: stage.id, - })); - }, - }, - - // Create Candidate Props - firstName: { - type: "string", - label: "First Name", - description: "Candidate's first name", - }, - lastName: { - type: "string", - label: "Last Name", - description: "Candidate's last name", - }, - emails: { - type: "string[]", - label: "Email Addresses", - description: "List of candidate's email addresses", - }, - jobPosition: { - type: "string", - label: "Job Position", - description: "The position the candidate is applying for", - optional: true, - async options() { - const positions = await this.listJobPositions(); - return positions.map((position) => ({ - label: position.name, - value: position.id, - })); - }, - }, - source: { - type: "string", - label: "Source", - description: "Source of the candidate", - optional: true, - }, - notes: { - type: "string", - label: "Notes", - description: "Additional notes about the candidate", - optional: true, - }, - // Update Candidate Stage Props - candidateId: { - type: "string", - label: "Candidate ID", - description: "The ID of the candidate to update", - }, - newStage: { - type: "string", - label: "New Stage", - description: "The new stage for the candidate", - async options({ monitorPipelines }) { - if (!monitorPipelines || monitorPipelines.length === 0) return []; - const stages = await this.listStages({ - pipelineIds: monitorPipelines, - }); - return stages.map((stage) => ({ - label: stage.name, - value: stage.id, + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, })); }, }, - changeNote: { - type: "string", - label: "Change Note", - description: "Optional note or reason for stage change", - optional: true, - }, }, methods: { - // Log Authentication Keys - authKeys() { - console.log(Object.keys(this.$auth)); - }, - - // Base URL Method _baseUrl() { return "https://api.gem.com/v0"; }, - - // Make Request Method - async _makeRequest(opts = {}) { - const { - $ = this, - method = "GET", - path = "/", - headers, - ...otherOpts - } = opts; - return axios($, { - method, - url: this._baseUrl() + path, - headers: { - ...headers, - Authorization: `Bearer ${this.$auth.api_key}`, - }, - ...otherOpts, - }); - }, - - // Pagination Method - async paginate(fn, ...opts) { - const results = []; - let page = 1; - let more = true; - while (more) { - const response = await fn({ - page, - ...opts, - }); - if (!response || response.length === 0) { - more = false; - } else { - results.push(...response); - page += 1; - } - } - return results; - }, - - // List Job Positions - async listJobPositions(opts = {}) { - return this.paginate(this.listJobPositionsPage, opts); + _headers() { + return { + "x-api-key": `${this.$auth.api_key}`, + "content-type": "application/json", + }; }, - async listJobPositionsPage({ - page, ...opts + _makeRequest({ + $ = this, path, ...opts }) { - return this._makeRequest({ - method: "GET", - path: `/job_positions?page=${page}`, + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), ...opts, }); }, - - // List Recruiters - async listRecruiters(opts = {}) { - return this.paginate(this.listRecruitersPage, opts); - }, - async listRecruitersPage({ - page, ...opts - }) { + listCandidates(opts = {}) { return this._makeRequest({ - method: "GET", - path: `/recruiters?page=${page}`, + path: "/candidates", ...opts, }); }, - - // List Pipelines - async listPipelines(opts = {}) { - return this.paginate(this.listPipelinesPage, opts); - }, - async listPipelinesPage({ - page, ...opts - }) { + listProjects(opts = {}) { return this._makeRequest({ - method: "GET", - path: `/pipelines?page=${page}`, + path: "/projects", ...opts, }); }, - - // List Stages - async listStages({ - pipelineIds = [], ...opts - }) { - if (pipelineIds.length === 0) return []; - const stagePromises = pipelineIds.map((pipelineId) => - this.paginate(this.listStagesPage, { - pipelineId, - ...opts, - })); - const stagesArrays = await Promise.all(stagePromises); - return stagesArrays.flat(); - }, - async listStagesPage({ - pipelineId, page, ...opts - }) { + listUsers(opts = {}) { return this._makeRequest({ - method: "GET", - path: `/pipelines/${pipelineId}/stages?page=${page}`, + path: "/users", ...opts, }); }, - - // Create a New Candidate - async createCandidate({ - firstName, lastName, emails, jobPosition, source, notes, - }) { - const data = { - first_name: firstName, - last_name: lastName, - emails: emails.map((email) => ({ - email_address: email, - is_primary: false, - })), - }; - if (jobPosition) data.title = jobPosition; - if (source) data.sourced_from = source; - if (notes) data.notes = notes; + createCandidate(opts = {}) { return this._makeRequest({ method: "POST", path: "/candidates", - data, + ...opts, }); }, - - // Update Candidate Stage - async updateCandidateStage({ - candidateId, newStage, changeNote, + async *paginate({ + fn, params = {}, maxResults = null, ...opts }) { - const data = { - stage: newStage, - }; - if (changeNote) data.notes = changeNote; - return this._makeRequest({ - method: "PUT", - path: `/candidates/${candidateId}`, - data, - }); + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + params.page_size = LIMIT; + const data = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = data.length === LIMIT; + + } while (hasMore); }, }, }; diff --git a/components/gem/package.json b/components/gem/package.json index 00acd25ff41d6..3dc22cd7541f4 100644 --- a/components/gem/package.json +++ b/components/gem/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/gem", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Gem Components", "main": "gem.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/gem/sources/new-candidate/new-candidate.mjs b/components/gem/sources/new-candidate/new-candidate.mjs index dabea1e9b04f1..a65b0744b9323 100644 --- a/components/gem/sources/new-candidate/new-candidate.mjs +++ b/components/gem/sources/new-candidate/new-candidate.mjs @@ -1,20 +1,16 @@ -import { - axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, -} from "@pipedream/platform"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; import gem from "../../gem.app.mjs"; +import sampleEmit from "./test-event.mjs"; export default { key: "gem-new-candidate", name: "New Candidate Added", - description: "Emit a new event when a candidate is added in Gem. [See the documentation]()", - version: "0.0.{{ts}}", + description: "Emit new event when a candidate is added in Gem. [See the documentation](https://api.gem.com/v0/reference#tag/Candidates/paths/~1v0~1candidates/get)", + version: "0.0.1", type: "source", dedupe: "unique", props: { - gem: { - type: "app", - app: "gem", - }, + gem, db: "$.service.db", timer: { type: "$.interface.timer", @@ -22,121 +18,52 @@ export default { intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, }, }, - filterJobPositions: { - propDefinition: [ - "gem", - "filterJobPositions", - ], + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 1; }, - filterRecruiters: { - propDefinition: [ - "gem", - "filterRecruiters", - ], + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); }, - }, - hooks: { - async deploy() { - const pageSize = 50; - let page = 1; - let candidates = []; - let more = true; + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); - while (more && candidates.length < pageSize) { - const fetchedCandidates = await this.gem.listCandidates({ - page, - perPage: pageSize, - }); - if (fetchedCandidates.length === 0) { - more = false; - } else { - candidates.push(...fetchedCandidates); - if (fetchedCandidates.length < pageSize) { - more = false; - } else { - page += 1; - } - } + const response = this.gem.paginate({ + fn: this.gem.listCandidates, + params: { + created_after: lastDate, + }, + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); } - const recentCandidates = candidates.slice(0, pageSize); - for (const candidate of recentCandidates) { - this.$emit( - candidate, - { - id: candidate.id, - summary: `New Candidate: ${candidate.first_name} ${candidate.last_name}`, - ts: new Date(candidate.created_at).getTime(), - }, - ); + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(responseArray[0].created_at); } - if (recentCandidates.length > 0) { - const latestTimestamp = new Date(recentCandidates[0].created_at).getTime(); - await this.db.set("lastTimestamp", latestTimestamp); + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: `New Candidate with ID: ${item.id}`, + ts: item.created_at, + }); } }, - async activate() { - // No webhook subscription needed for polling source - }, - async deactivate() { - // No webhook subscription to remove for polling source + }, + hooks: { + async deploy() { + await this.emitEvent(25); }, }, async run() { - const lastTimestamp = (await this.db.get("lastTimestamp")) || 0; - let page = 1; - const newCandidates = []; - let more = true; - const pageSize = 50; - - while (more && newCandidates.length < pageSize) { - const fetchedCandidates = await this.gem.listCandidates({ - page, - perPage: pageSize, - createdAfter: lastTimestamp, - job_position_ids: this.filterJobPositions || [], - recruiter_ids: this.filterRecruiters || [], - }); - if (fetchedCandidates.length === 0) { - more = false; - } else { - for (const candidate of fetchedCandidates) { - const candidateTimestamp = new Date(candidate.created_at).getTime(); - if (candidateTimestamp > lastTimestamp) { - newCandidates.push(candidate); - if (newCandidates.length >= pageSize) { - break; - } - } - } - if (fetchedCandidates.length < pageSize) { - more = false; - } else { - page += 1; - } - } - } - - for (const candidate of newCandidates) { - this.$emit( - candidate, - { - id: candidate.id, - summary: `New Candidate: ${candidate.first_name} ${candidate.last_name}`, - ts: new Date(candidate.created_at).getTime(), - }, - ); - } - - if (newCandidates.length > 0) { - const latestTimestamp = newCandidates.reduce((max, candidate) => { - const candidateTime = new Date(candidate.created_at).getTime(); - return candidateTime > max - ? candidateTime - : max; - }, lastTimestamp); - await this.db.set("lastTimestamp", latestTimestamp); - } + await this.emitEvent(); }, + sampleEmit, }; diff --git a/components/gem/sources/new-candidate/test-event.mjs b/components/gem/sources/new-candidate/test-event.mjs new file mode 100644 index 0000000000000..bf438d98a1a4c --- /dev/null +++ b/components/gem/sources/new-candidate/test-event.mjs @@ -0,0 +1,77 @@ +export default { + "id": "string", + "created_at": 1, + "created_by": "string", + "last_updated_at": 1, + "candidate_greenhouse_id": "string", + "first_name": "string", + "last_name": "string", + "nickname": "string", + "weblink": "string", + "emails": [ + { + "email_address": "user@example.com", + "is_primary": false + } + ], + "phone_number": "string", + "location": "string", + "linked_in_handle": "string", + "profiles": [ + { + "network": "string", + "url": "string", + "username": "string" + } + ], + "company": "string", + "title": "string", + "school": "string", + "education_info": [ + { + "school": "string", + "parsed_university": "string", + "parsed_school": "string", + "start_date": "2019-08-24", + "end_date": "2019-08-24", + "field_of_study": "string", + "parsed_major_1": "string", + "parsed_major_2": "string", + "degree": "string" + } + ], + "work_info": [ + { + "company": "string", + "title": "string", + "work_start_date": "2019-08-24", + "work_end_date": "2019-08-24", + "is_current": true + } + ], + "custom_fields": [ + { + "id": "string", + "name": "string", + "scope": "team", + "project_id": "string", + "value": null, + "value_type": "date", + "value_option_ids": [ + "string" + ], + "custom_field_category": "string", + "custom_field_value": null + } + ], + "due_date": { + "date": "2019-08-24", + "user_id": "string", + "note": "string" + }, + "project_ids": [ + "string" + ], + "sourced_from": "SeekOut", + "gem_source": "SeekOut" +} \ No newline at end of file diff --git a/components/gem/sources/new-stage-change/new-stage-change.mjs b/components/gem/sources/new-stage-change/new-stage-change.mjs deleted file mode 100644 index 81607294eb68d..0000000000000 --- a/components/gem/sources/new-stage-change/new-stage-change.mjs +++ /dev/null @@ -1,105 +0,0 @@ -import { - axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, -} from "@pipedream/platform"; -import gem from "../../gem.app.mjs"; - -export default { - key: "gem-new-stage-change", - name: "New Stage Change", - description: "Emit a new event when a candidate's stage changes in a hiring pipeline. [See the documentation]()", - version: "0.0.{{ts}}", - type: "source", - dedupe: "unique", - props: { - gem: { - type: "app", - app: "gem", - }, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - monitorPipelines: { - propDefinition: [ - "gem", - "monitorPipelines", - ], - }, - monitorStages: { - propDefinition: [ - "gem", - "monitorStages", - (c) => ({ - monitorPipelines: c.monitorPipelines, - }), - ], - }, - }, - hooks: { - async deploy() { - const candidates = await this.gem.listCandidates({ - monitorPipelines: this.monitorPipelines, - }); - - const stageData = {}; - - const sortedCandidates = candidates.sort((a, b) => new Date(b.last_updated_at) - new Date(a.last_updated_at)); - - const recentCandidates = sortedCandidates.slice(0, 50); - - for (const candidate of recentCandidates) { - this.$emit( - candidate, - { - id: candidate.id, - summary: `Candidate ${candidate.firstName} ${candidate.lastName} moved to stage ${candidate.stage}.`, - ts: Date.parse(candidate.last_updated_at) || Date.now(), - }, - ); - stageData[candidate.id] = candidate.stage; - } - - await this.db.set("candidates", stageData); - }, - async activate() { - // No webhook setup required for polling source - }, - async deactivate() { - // No webhook teardown required for polling source - }, - }, - async run() { - const lastStageData = (await this.db.get("candidates")) || {}; - const candidates = await this.gem.listCandidates({ - monitorPipelines: this.monitorPipelines, - }); - - const newStageData = {}; - - for (const candidate of candidates) { - newStageData[candidate.id] = candidate.stage; - - const previousStage = lastStageData[candidate.id]; - if (previousStage && previousStage !== candidate.stage) { - if ( - this.monitorStages.length === 0 || - this.monitorStages.includes(candidate.stage) - ) { - this.$emit( - candidate, - { - id: candidate.id, - summary: `Candidate ${candidate.firstName} ${candidate.lastName} moved to stage ${candidate.stage}.`, - ts: Date.parse(candidate.last_updated_at) || Date.now(), - }, - ); - } - } - } - - await this.db.set("candidates", newStageData); - }, -}; From 4c59ee91c6df9f6f65c0f2e8d0eab27bf12f49d0 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Fri, 28 Feb 2025 15:01:32 -0300 Subject: [PATCH 3/5] pnpm update --- pnpm-lock.yaml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff2b326b30237..5c7b3f093e67c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1659,8 +1659,7 @@ importers: specifier: ^3.0.0 version: 3.0.3 - components/botx: - specifiers: {} + components/botx: {} components/bouncer: dependencies: @@ -3599,8 +3598,7 @@ importers: components/dokan: {} - components/domain_group: - specifiers: {} + components/domain_group: {} components/domo: {} @@ -4846,7 +4844,10 @@ importers: components/geckoboard: {} components/gem: - specifiers: {} + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/gemini_public: dependencies: @@ -4923,8 +4924,7 @@ importers: components/getswift: {} - components/getty_images: - specifiers: {} + components/getty_images: {} components/ghost_org_admin_api: dependencies: @@ -5587,8 +5587,7 @@ importers: specifier: ^1.6.5 version: 1.6.6 - components/greenhouse_job_board_api: - specifiers: {} + components/greenhouse_job_board_api: {} components/greptile: dependencies: @@ -9473,8 +9472,7 @@ importers: specifier: ^1.6.0 version: 1.6.6 - components/planday: - specifiers: {} + components/planday: {} components/planhat: {} @@ -14786,7 +14784,7 @@ importers: version: 3.1.7 ts-jest: specifier: ^29.2.5 - version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2) + version: 29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2) tsup: specifier: ^8.3.6 version: 8.3.6(@microsoft/api-extractor@7.47.12(@types/node@20.17.6))(jiti@1.21.6)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.6.1) @@ -34052,6 +34050,8 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) + transitivePeerDependencies: + - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: @@ -46833,7 +46833,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2): + ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -46847,10 +46847,10 @@ snapshots: typescript: 5.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.26.0 + '@babel/core': 8.0.0-alpha.13 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.26.0) + babel-jest: 29.7.0(@babel/core@8.0.0-alpha.13) esbuild: 0.24.2 ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.6.3): From c46e779b174d5c4238d9778310981733f64bf671 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Fri, 28 Feb 2025 15:04:21 -0300 Subject: [PATCH 4/5] pnpm update --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c7b3f093e67c..d4b3fb08f50b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14784,7 +14784,7 @@ importers: version: 3.1.7 ts-jest: specifier: ^29.2.5 - version: 29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2) + version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2) tsup: specifier: ^8.3.6 version: 8.3.6(@microsoft/api-extractor@7.47.12(@types/node@20.17.6))(jiti@1.21.6)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.6.1) @@ -46833,7 +46833,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2): + ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -46847,10 +46847,10 @@ snapshots: typescript: 5.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 8.0.0-alpha.13 + '@babel/core': 7.26.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@8.0.0-alpha.13) + babel-jest: 29.7.0(@babel/core@7.26.0) esbuild: 0.24.2 ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.6.3): From f64ac07c6d843de27613d06a3359bf9ed25db513 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Fri, 28 Feb 2025 15:43:24 -0300 Subject: [PATCH 5/5] pnpm update --- pnpm-lock.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f0e524b73b1f..1729e5711a987 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1662,7 +1662,6 @@ importers: version: 3.0.3 components/botx: {} - components/botx: {} components/bouncer: dependencies: