diff --git a/components/breathe/actions/approve-or-reject-leave-request/approve-or-reject-leave-request.mjs b/components/breathe/actions/approve-or-reject-leave-request/approve-or-reject-leave-request.mjs new file mode 100644 index 0000000000000..d6f7ab71a7763 --- /dev/null +++ b/components/breathe/actions/approve-or-reject-leave-request/approve-or-reject-leave-request.mjs @@ -0,0 +1,72 @@ +import breathe from "../../breathe.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "breathe-approve-or-reject-leave-request", + name: "Approve or Reject Leave Request", + description: "Approve or reject an employee leave request in Breathe. [See the documentation](https://developer.breathehr.com/examples#!/leave_requests)", + version: "0.0.1", + type: "action", + props: { + breathe, + employeeId: { + propDefinition: [ + breathe, + "employeeId", + ], + }, + leaveRequestId: { + propDefinition: [ + breathe, + "leaveRequestId", + (c) => ({ + employeeId: c.employeeId, + }), + ], + }, + approveOrReject: { + type: "string", + label: "Approve or Reject?", + description: "Whether to approve or reject the leave request", + options: [ + "approve", + "reject", + ], + }, + rejectionReason: { + type: "string", + label: "Rejection Reason", + description: "The reason for rejecting the leave request", + optional: true, + }, + }, + async run({ $ }) { + if (this.approveOrReject === "reject" && !this.rejectionReason) { + throw new ConfigurationError("Rejection Reason is required if rejecting the leave request"); + } + + let response; + if (this.approveOrReject === "reject") { + response = await this.breathe.rejectLeaveRequest({ + $, + leaveRequestId: this.leaveRequestId, + data: { + leave_request: { + rejection_reason: this.rejectionReason, + }, + }, + }); + } + else { + response = await this.breathe.approveLeaveRequest({ + $, + leaveRequestId: this.leaveRequestId, + }); + } + + $.export("$summary", `Successfully ${this.approveOrReject === "reject" + ? "rejected" + : "approved"} leave request with ID: ${this.leaveRequestId}`); + return response; + }, +}; diff --git a/components/breathe/actions/create-employee/create-employee.mjs b/components/breathe/actions/create-employee/create-employee.mjs new file mode 100644 index 0000000000000..47d61cc617e52 --- /dev/null +++ b/components/breathe/actions/create-employee/create-employee.mjs @@ -0,0 +1,118 @@ +import breathe from "../../breathe.app.mjs"; + +export default { + key: "breathe-create-employee", + name: "Create Employee", + description: "Creates a new employee in Breathe. [See the documentation](https://developer.breathehr.com/examples#!/employees/POST_version_employees_json)", + version: "0.0.1", + type: "action", + props: { + breathe, + firstName: { + type: "string", + label: "First Name", + description: "First name of the employee", + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the employee", + }, + email: { + type: "string", + label: "Email", + description: "Email address of the employee", + }, + companyJoinDate: { + type: "string", + label: "Company Join Date", + description: "The date that the employee joined the company. Example: `2023-12-25`", + }, + dob: { + type: "string", + label: "Date of Birth", + description: "The date of birth of the employee", + optional: true, + }, + jobTitle: { + type: "string", + label: "Job Title", + description: "The job title of the employee", + optional: true, + }, + departmentId: { + propDefinition: [ + breathe, + "departmentId", + ], + }, + divisionId: { + propDefinition: [ + breathe, + "divisionId", + ], + }, + locationId: { + propDefinition: [ + breathe, + "locationId", + ], + }, + workingPatternId: { + propDefinition: [ + breathe, + "workingPatternId", + ], + }, + holidayAllowanceId: { + propDefinition: [ + breathe, + "holidayAllowanceId", + ], + }, + workMobile: { + type: "string", + label: "Work Mobile", + description: "The work moblie phone number of the employee", + optional: true, + }, + personalMobile: { + type: "string", + label: "Personal Mobile", + description: "The personal mobile phone number of the employee", + optional: true, + }, + homeTelephone: { + type: "string", + label: "Home Telephone", + description: "The home telephone number of the employee", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.breathe.createEmployee({ + $, + data: { + employee: { + person_type: "Employee", + first_name: this.firstName, + last_name: this.lastName, + email: this.email, + company_join_date: this.companyJoinDate, + dob: this.dob, + job_title: this.jobTitle, + work_mobile: this.workMobile, + personal_mobile: this.personalMobile, + home_telephone: this.homeTelephone, + department: this.departmentId, + division: this.divisionId, + location: this.locationId, + working_pattern: this.workingPatternId, + holiday_allowance: this.holidayAllowanceId, + }, + }, + }); + $.export("$summary", `Successfully created employee with ID: ${response.employees[0].id}`); + return response; + }, +}; diff --git a/components/breathe/actions/create-leave-request/create-leave-request.mjs b/components/breathe/actions/create-leave-request/create-leave-request.mjs new file mode 100644 index 0000000000000..3520598756e8d --- /dev/null +++ b/components/breathe/actions/create-leave-request/create-leave-request.mjs @@ -0,0 +1,57 @@ +import breathe from "../../breathe.app.mjs"; + +export default { + key: "breathe-create-leave-request", + name: "Create Leave Request", + description: "Creates a new leave request for an employee in Breathe. [See the documentation](https://developer.breathehr.com/examples#!/employees/POST_version_employees_id_leave_requests_json)", + version: "0.0.1", + type: "action", + props: { + breathe, + employeeId: { + propDefinition: [ + breathe, + "employeeId", + ], + }, + startDate: { + type: "string", + label: "Start Date", + description: "The start date of the leave request. Example: `2023-12-25`", + }, + endDate: { + type: "string", + label: "End Date", + description: "The end date of the leave request. Example: `2023-12-27`", + }, + halfStart: { + type: "boolean", + label: "Half Start", + description: "Set to `true` if requesting a half-day", + default: false, + optional: true, + }, + notes: { + type: "string", + label: "Notes", + description: "Notes about the leave request", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.breathe.createLeaveRequest({ + $, + employeeId: this.employeeId, + data: { + leave_request: { + start_date: this.startDate, + end_date: this.endDate, + half_start: this.halfStart, + notes: this.notes, + }, + }, + }); + $.export("$summary", `Successfully created leave for employee with ID: ${this.employeeId}`); + return response; + }, +}; diff --git a/components/breathe/breathe.app.mjs b/components/breathe/breathe.app.mjs index 0bb31c78214d3..aaedd80aa5521 100644 --- a/components/breathe/breathe.app.mjs +++ b/components/breathe/breathe.app.mjs @@ -1,11 +1,244 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "breathe", - propDefinitions: {}, + propDefinitions: { + employeeId: { + type: "string", + label: "Employee ID", + description: "The identifier of an employee", + async options({ page }) { + const { employees } = await this.listEmployees({ + params: { + page: page + 1, + }, + }); + return employees?.map(({ + id: value, first_name: firstName, last_name: lastName, + }) => ({ + value, + label: `${firstName} ${lastName}`, + })) || []; + }, + }, + leaveRequestId: { + type: "string", + label: "Leave Request ID", + description: "The identifier of a leave request", + async options({ + page, employeeId, + }) { + const { leave_requests: leaveRequests } = await this.listLeaveRequests({ + params: { + page: page + 1, + employee_id: employeeId, + exclude_cancelled_requests: true, + }, + }); + return leaveRequests?.map(({ + id: value, start_date: startDate, end_date: endDate, + }) => ({ + value, + label: `${startDate} - ${endDate}`, + })) || []; + }, + }, + departmentId: { + type: "string", + label: "Department ID", + description: "The identifier of a department", + optional: true, + async options({ page }) { + const { departments } = await this.listDepartments({ + params: { + page: page + 1, + }, + }); + return departments?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + divisionId: { + type: "string", + label: "Division ID", + description: "The identifier of a division", + optional: true, + async options() { + const { divisions } = await this.listDivisions(); + return divisions?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + locationId: { + type: "string", + label: "Location ID", + description: "The identifier of a location", + optional: true, + async options() { + const { locations } = await this.listLocations(); + return locations?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + workingPatternId: { + type: "string", + label: "Working Pattern ID", + description: "The identifier of a working pattern", + optional: true, + async options() { + const { working_patterns: workingPatterns } = await this.listWorkingPatterns(); + return workingPatterns?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + holidayAllowanceId: { + type: "string", + label: "Holiday Allowance ID", + description: "The identifier of a holiday allowance", + optional: true, + async options() { + const { holiday_allowances: holidayAllowances } = await this.listHolidayAllowances(); + return holidayAllowances?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `${this.$auth.api_url}/v1`; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "x-api-key": `${this.$auth.api_key}`, + }, + }); + }, + listEmployees(opts = {}) { + return this._makeRequest({ + path: "/employees", + ...opts, + }); + }, + listDepartments(opts = {}) { + return this._makeRequest({ + path: "/departments", + ...opts, + }); + }, + listDivisions(opts = {}) { + return this._makeRequest({ + path: "/divisions", + ...opts, + }); + }, + listLocations(opts = {}) { + return this._makeRequest({ + path: "/locations", + ...opts, + }); + }, + listWorkingPatterns(opts = {}) { + return this._makeRequest({ + path: "/working_patterns", + ...opts, + }); + }, + listHolidayAllowances(opts = {}) { + return this._makeRequest({ + path: "/holiday_allowances", + ...opts, + }); + }, + listLeaveRequests(opts = {}) { + return this._makeRequest({ + path: "/leave_requests", + ...opts, + }); + }, + createEmployee(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/employees", + ...opts, + }); + }, + createLeaveRequest({ + employeeId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/employees/${employeeId}/leave_requests`, + ...opts, + }); + }, + approveLeaveRequest({ + leaveRequestId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/leave_requests/${leaveRequestId}/approve`, + ...opts, + }); + }, + rejectLeaveRequest({ + leaveRequestId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/leave_requests/${leaveRequestId}/reject`, + ...opts, + }); + }, + async *paginate({ + resourceFn, + args, + resourceKey, + }) { + args = { + ...args, + params: { + ...args?.params, + page: 1, + }, + }; + let total; + do { + const response = await resourceFn(args); + const items = response[resourceKey]; + for (const item of items) { + yield item; + } + total = items?.length; + args.params.page++; + } while (total); }, }, }; diff --git a/components/breathe/package.json b/components/breathe/package.json index 8f75a204a33f6..352d266e38dd3 100644 --- a/components/breathe/package.json +++ b/components/breathe/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/breathe", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Breathe Components", "main": "breathe.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/breathe/sources/common/base.mjs b/components/breathe/sources/common/base.mjs new file mode 100644 index 0000000000000..629e8a6e07109 --- /dev/null +++ b/components/breathe/sources/common/base.mjs @@ -0,0 +1,69 @@ +import breathe from "../../breathe.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + breathe, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || "1970-01-01"; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getArgs() { + return {}; + }, + isRelevant() { + return true; + }, + getTsField() { + throw new Error("getTsField is not implemented"); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getResourceKey() { + throw new Error("getResourceKey is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + + const tsField = this.getTsField(); + + const results = this.breathe.paginate({ + resourceFn: this.getResourceFn(), + args: this.getArgs(lastTs), + resourceKey: this.getResourceKey(), + }); + + for await (const item of results) { + const ts = Date.parse(item[tsField]); + if (ts >= Date.parse(lastTs)) { + if (this.isRelevant(item)) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + } + + if (ts > Date.parse(maxTs)) { + maxTs = item[tsField]; + } + } + } + + this._setLastTs(maxTs); + }, +}; diff --git a/components/breathe/sources/employee-updated/employee-updated.mjs b/components/breathe/sources/employee-updated/employee-updated.mjs new file mode 100644 index 0000000000000..2cf02beca7b65 --- /dev/null +++ b/components/breathe/sources/employee-updated/employee-updated.mjs @@ -0,0 +1,34 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "breathe-employee-updated", + name: "Employee Updated", + description: "Emit new event when an existing employee is updated in Breathe. [See the documentation](https://developer.breathehr.com/examples#!/employees/GET_version_employees_json)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTsField() { + return "updated_at"; + }, + getResourceFn() { + return this.breathe.listEmployees; + }, + getResourceKey() { + return "employees"; + }, + isRelevant(employee) { + return !(employee.created_at === employee.updated_at); + }, + generateMeta(employee) { + const ts = Date.parse(employee.updated_at); + return { + id: `${employee.id}-${ts}`, + summary: `Employee Updated: ${employee.first_name} ${employee.last_name}`, + ts, + }; + }, + }, +}; diff --git a/components/breathe/sources/new-employee-created/new-employee-created.mjs b/components/breathe/sources/new-employee-created/new-employee-created.mjs new file mode 100644 index 0000000000000..60dbe5c57812e --- /dev/null +++ b/components/breathe/sources/new-employee-created/new-employee-created.mjs @@ -0,0 +1,30 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "breathe-new-employee-created", + name: "New Employee Created", + description: "Emit new event when a new employee is created in Breathe. [See the documentation](https://developer.breathehr.com/examples#!/employees/GET_version_employees_json).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTsField() { + return "created_at"; + }, + getResourceFn() { + return this.breathe.listEmployees; + }, + getResourceKey() { + return "employees"; + }, + generateMeta(employee) { + return { + id: employee.id, + summary: `New Employee: ${employee.first_name} ${employee.last_name}`, + ts: Date.parse(employee.created_at), + }; + }, + }, +}; diff --git a/components/breathe/sources/new-leave-request-created/new-leave-request-created.mjs b/components/breathe/sources/new-leave-request-created/new-leave-request-created.mjs new file mode 100644 index 0000000000000..41e83fa1f43c8 --- /dev/null +++ b/components/breathe/sources/new-leave-request-created/new-leave-request-created.mjs @@ -0,0 +1,57 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "breathe-new-leave-request-created", + name: "New Leave Request Created", + description: "Emit new event when a new employee leave request is created in Breathe. [See the documentation](https://developer.breathehr.com/examples#!/employees/GET_version_employees_id_leave_requests_json)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + employeeIds: { + propDefinition: [ + common.props.breathe, + "employeeId", + ], + type: "string[]", + label: "Employee IDs", + description: "Return leave requests for the selected employees only. If no employees are selected, leave requests for all employees will be returned.", + optional: true, + }, + }, + methods: { + ...common.methods, + getTsField() { + return "created_at"; + }, + getResourceFn() { + return this.breathe.listLeaveRequests; + }, + getArgs(lastTs) { + const args = { + params: { + startDate: lastTs, + }, + }; + if (this.employeeIds?.length === 1) { + args.params.employee_id = this.employeeIds[0]; + } + return args; + }, + getResourceKey() { + return "leave_requests"; + }, + isRelevant(leaveRequest) { + return !this.employeeIds?.length || this.employeeIds.includes(leaveRequest.employee.id); + }, + generateMeta(leaveRequest) { + return { + id: leaveRequest.id, + summary: `New Leave Request ID: ${leaveRequest.id}`, + ts: Date.parse(leaveRequest.created_at), + }; + }, + }, +}; diff --git a/components/richpanel/richpanel.app.mjs b/components/richpanel/richpanel.app.mjs index 977736076b668..8b09379770ea3 100644 --- a/components/richpanel/richpanel.app.mjs +++ b/components/richpanel/richpanel.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b0526870c357..77f809260be07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1338,7 +1338,11 @@ importers: components/braze: {} - components/breathe: {} + components/breathe: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/brevo: dependencies: @@ -8672,8 +8676,7 @@ importers: specifier: ^1.5.1 version: 1.6.6 - components/richpanel: - specifiers: {} + components/richpanel: {} components/ringcentral: dependencies: