diff --git a/components/homerun/actions/add-candidate-note/add-candidate-note.mjs b/components/homerun/actions/add-candidate-note/add-candidate-note.mjs new file mode 100644 index 0000000000000..cfe0cd72a6633 --- /dev/null +++ b/components/homerun/actions/add-candidate-note/add-candidate-note.mjs @@ -0,0 +1,58 @@ +import app from "../../homerun.app.mjs"; + +export default { + key: "homerun-add-candidate-note", + name: "Add Candidate Note", + description: "Adds a note to a candidate's profile in Homerun. [See the documentation](https://developers.homerun.co/#tag/Job-Application-Notes/operation/job-applications.job-application-id.notes.post).", + version: "0.0.1", + type: "action", + props: { + app, + jobApplicationId: { + propDefinition: [ + app, + "jobApplicationId", + ], + }, + note: { + type: "string", + label: "Note Content", + description: "The content of the note to add.", + }, + displayName: { + type: "string", + label: "Display Name", + description: "Name of the note's author.", + }, + }, + methods: { + addCandidateNote({ + jobApplicationId, ...args + } = {}) { + return this.app.post({ + path: `/job-applications/${jobApplicationId}/notes`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + addCandidateNote, + jobApplicationId, + note, + displayName, + } = this; + + const response = await addCandidateNote({ + $, + jobApplicationId, + data: { + note, + display_name: displayName, + }, + }); + + $.export("$summary", "Successfully added a note."); + return response; + }, +}; diff --git a/components/homerun/actions/create-job-application/create-job-application.mjs b/components/homerun/actions/create-job-application/create-job-application.mjs new file mode 100644 index 0000000000000..ca099128a8542 --- /dev/null +++ b/components/homerun/actions/create-job-application/create-job-application.mjs @@ -0,0 +1,144 @@ +import app from "../../homerun.app.mjs"; + +export default { + key: "homerun-create-job-application", + name: "Create Job Application", + description: "Creates a new job application. [See the documentation](https://developers.homerun.co/#tag/Job-Applications/operation/job-applications.post).", + version: "0.0.1", + type: "action", + props: { + app, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the applicant. Make sure you don't include numbers or special characters.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the applicant. Make sure you don't include numbers or special characters.", + }, + email: { + type: "string", + label: "Email", + description: "The email of the applicant.", + }, + dateOfBirth: { + type: "string", + label: "Date of Birth", + description: "The date of birth of the applicant in the format of `YYYY-MM-DD`.", + optional: true, + }, + vacancyId: { + optional: true, + propDefinition: [ + app, + "vacancyId", + ], + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the applicant.", + optional: true, + }, + photo: { + type: "string", + label: "Photo", + description: "The URL of the applicant's photo.", + optional: true, + }, + experience: { + type: "string", + label: "Experience", + description: "The experience of the applicant.", + optional: true, + }, + education: { + type: "string", + label: "Education", + description: "The education of the applicant.", + optional: true, + }, + facebook: { + type: "string", + label: "Facebook", + description: "The Facebook URL of the applicant. eg. `https://facebook.com/username`", + optional: true, + }, + twitter: { + type: "string", + label: "X", + description: "The X URL of the applicant. eg. `https://x.com/username`", + optional: true, + }, + linkedin: { + type: "string", + label: "LinkedIn", + description: "The LinkedIn URL of the applicant. eg. `https://linkedin.com/in/username`", + optional: true, + }, + github: { + type: "string", + label: "GitHub", + description: "The GitHub URL of the applicant. eg. `https://github.com/username`", + optional: true, + }, + website: { + type: "string", + label: "Website", + description: "The website URL of the applicant. eg. `https://username.com`", + optional: true, + }, + }, + methods: { + createJobApplication(args = {}) { + return this.app.post({ + path: "/job-applications", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createJobApplication, + firstName, + lastName, + email, + dateOfBirth, + vacancyId, + phoneNumber, + photo, + experience, + education, + facebook, + twitter, + linkedin, + github, + website, + } = this; + + const response = await createJobApplication({ + $, + data: { + vacancyId, + first_name: firstName, + last_name: lastName, + email, + date_of_birth: dateOfBirth, + phone_number: phoneNumber, + photo, + experience, + education, + facebook, + twitter, + linkedin, + github, + website, + }, + }); + + $.export("$summary", "Successfully created a job application."); + return response; + }, +}; diff --git a/components/homerun/common/constants.mjs b/components/homerun/common/constants.mjs new file mode 100644 index 0000000000000..06aa3cf920c69 --- /dev/null +++ b/components/homerun/common/constants.mjs @@ -0,0 +1,14 @@ +const BASE_URL = "https://api.homerun.co"; +const VERSION_PATH = "/v2"; +const DEFAULT_LIMIT = 100; +const DEFAULT_MAX = 1000; + +const LAST_DATE_AT = "lastDateAt"; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_LIMIT, + DEFAULT_MAX, + LAST_DATE_AT, +}; diff --git a/components/homerun/common/utils.mjs b/components/homerun/common/utils.mjs new file mode 100644 index 0000000000000..de7ee6c4a4692 --- /dev/null +++ b/components/homerun/common/utils.mjs @@ -0,0 +1,17 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +function getNestedProperty(obj, propertyString) { + const properties = propertyString.split("."); + return properties.reduce((prev, curr) => prev?.[curr], obj); +} + +export default { + iterate, + getNestedProperty, +}; diff --git a/components/homerun/homerun.app.mjs b/components/homerun/homerun.app.mjs index a04fd2ae8ebc4..947a394e2bc2e 100644 --- a/components/homerun/homerun.app.mjs +++ b/components/homerun/homerun.app.mjs @@ -1,11 +1,143 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + export default { type: "app", app: "homerun", - propDefinitions: {}, + propDefinitions: { + vacancyId: { + type: "string", + label: "Vacancy ID", + description: "The ID of the vacancy.", + async options({ page }) { + const { data } = await this.listVacancies({ + params: { + page: page + 1, + }, + }); + return data.map(({ + id: value, + title: label, + }) => ({ + label, + value, + })); + }, + }, + jobApplicationId: { + type: "string", + label: "Job Application ID", + description: "The ID of the job application.", + async options({ page }) { + const { data } = await this.listJobApplications({ + params: { + page: page + 1, + }, + }); + return data.map(({ + id: value, + personal_info: { + first_name: firstName, + last_name: lastName, + email, + }, + }) => ({ + label: `${firstName} ${lastName} (${email})`, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listVacancies(args = {}) { + return this._makeRequest({ + path: "/vacancies", + ...args, + }); + }, + listJobApplications(args = {}) { + return this._makeRequest({ + path: "/job-applications", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + lastDateAt, dateField, + max = constants.DEFAULT_MAX, + }) { + let page = 1; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + page, + perPage: constants.DEFAULT_LIMIT, + }, + }); + + const nextResources = utils.getNestedProperty(response, resourceName); + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + const isDateGreaterThanLasDate = + lastDateAt + && Date.parse(resource[dateField]) > Date.parse(lastDateAt); + + if (!lastDateAt || isDateGreaterThanLasDate) { + yield resource; + resourcesCount += 1; + } + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page found"); + return; + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); }, }, }; diff --git a/components/homerun/package.json b/components/homerun/package.json index 2aad327e3fb02..f772b26e33911 100644 --- a/components/homerun/package.json +++ b/components/homerun/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/homerun", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Homerun Components", "main": "homerun.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } -} \ No newline at end of file +} diff --git a/components/homerun/sources/common/polling.mjs b/components/homerun/sources/common/polling.mjs new file mode 100644 index 0000000000000..976a0a9ff291f --- /dev/null +++ b/components/homerun/sources/common/polling.mjs @@ -0,0 +1,95 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../homerun.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + setLastDateAt(value) { + this.db.set(constants.LAST_DATE_AT, value); + }, + getLastDateAt() { + return this.db.get(constants.LAST_DATE_AT); + }, + getDateField() { + throw new ConfigurationError("getDateField is not implemented"); + }, + isResourceRelevant() { + return true; + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + async processResources(resources) { + const { + isResourceRelevant, + processResource, + } = this; + + return Array.from(resources) + .filter(isResourceRelevant) + .forEach(processResource); + }, + }, + async run() { + const { + app, + getDateField, + getLastDateAt, + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResources, + setLastDateAt, + } = this; + + const dateField = getDateField(); + const lastDateAt = getLastDateAt(); + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + resourceName: getResourceName(), + dateField, + lastDateAt, + }); + + if (resources.length) { + const [ + firstResource, + ] = Array.from(resources).reverse(); + if (firstResource) { + setLastDateAt(firstResource[dateField]); + } + } + + processResources(resources); + }, +}; diff --git a/components/homerun/sources/new-job-application-created/new-job-application-created.mjs b/components/homerun/sources/new-job-application-created/new-job-application-created.mjs new file mode 100644 index 0000000000000..882a61cfc3108 --- /dev/null +++ b/components/homerun/sources/new-job-application-created/new-job-application-created.mjs @@ -0,0 +1,35 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "homerun-new-job-application-created", + name: "New Job Application Created", + description: "Emit new event when a candidate submits an application for a job posting. [See the documentation](https://developers.homerun.co/#tag/Job-Applications/operation/job-applications.index).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "created_at"; + }, + getResourceName() { + return "data"; + }, + getResourcesFn() { + return this.app.listJobApplications; + }, + getResourcesFnArgs() { + return {}; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Job Application: ${resource.personal_info.email}`, + ts: Date.parse(resource.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/homerun/sources/new-job-application-created/test-event.mjs b/components/homerun/sources/new-job-application-created/test-event.mjs new file mode 100644 index 0000000000000..92b606c4978a0 --- /dev/null +++ b/components/homerun/sources/new-job-application-created/test-event.mjs @@ -0,0 +1,55 @@ +export default { + "id": "string", + "rating": 0, + "retention_period_ends_at": "2019-08-24T14:15:22Z", + "sourced": true, + "total_votes": 0, + "homerun_url": "string", + "created_at": "2019-08-24T14:15:22Z", + "disqualified": true, + "hired_at": "2019-08-24T14:15:22Z", + "first_reply_at": "2019-08-24T14:15:22Z", + "personal_info": { + "first_name": "string", + "last_name": "string", + "email": "user@example.com", + "city": "string", + "country": "string", + "date_of_birth": "2019-08-24", + "phone_number": "string", + "photo": "string" + }, + "vacancy_id": "string", + "stage": { + "name": "string" + }, + "notes": [ + { + "display_name": "string", + "is_sensitive": true, + "note": "string", + "created_at": "2019-08-24T14:15:22Z", + "updated_at": "2019-08-24T14:15:22Z" + } + ], + "sources": [ + { + "name": "Homerun Career Page" + } + ], + "disqualification_reason": { + "name": "Too junior" + }, + "question_answers": [ + { + "language": "English", + "original_question": "Can you describe your ideal work environment?", + "original_question_type": "file", + "answer": { + "name": "my-resume.pdf", + "type": "resume", + "temporary_download_url": "string" + } + } + ] +}; diff --git a/components/homerun/sources/new-vacancy-created/new-vacancy-created.mjs b/components/homerun/sources/new-vacancy-created/new-vacancy-created.mjs new file mode 100644 index 0000000000000..a96f85389a1bc --- /dev/null +++ b/components/homerun/sources/new-vacancy-created/new-vacancy-created.mjs @@ -0,0 +1,35 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "homerun-new-vacancy-created", + name: "New Vacancy Created", + description: "Emit new event when a new vacancy is created. [See the documentation](https://developers.homerun.co/#tag/Vacancies/operation/vacancies.get).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "created_at"; + }, + getResourceName() { + return "data"; + }, + getResourcesFn() { + return this.app.listVacancies; + }, + getResourcesFnArgs() { + return {}; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Vacancy: ${resource.title}`, + ts: Date.parse(resource.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/homerun/sources/new-vacancy-created/test-event.mjs b/components/homerun/sources/new-vacancy-created/test-event.mjs new file mode 100644 index 0000000000000..ceaac36ce9e30 --- /dev/null +++ b/components/homerun/sources/new-vacancy-created/test-event.mjs @@ -0,0 +1,32 @@ +export default { + "id": "string", + "application_form_url": "http://example.com", + "job_url": "http://example.com", + "share_image_url": "http://example.com", + "status": "public", + "title": "string", + "description": "string", + "page_content": "string", + "total_candidate_count": 0, + "type": "Contracted", + "expires_at": "2019-08-24T14:15:22Z", + "is_remote": true, + "location_type": "on-site", + "location": { + "name": "string", + "country": "string", + "address": "string", + "postal_code": "string", + "city": "string", + "region": "string" + }, + "department": { + "name": "string" + }, + "stages": [ + { + "name": "string" + } + ], + "created_at": "2019-08-24T14:15:22Z" +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7062164d7b4dc..efd8e9a306637 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4998,7 +4998,11 @@ importers: specifier: ^1.6.0 version: 1.6.6 - components/homerun: {} + components/homerun: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/honeyhive: {} @@ -32338,8 +32342,6 @@ 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: