diff --git a/components/hubstaff/actions/create-task/create-task.mjs b/components/hubstaff/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..156a0ced63ad9 --- /dev/null +++ b/components/hubstaff/actions/create-task/create-task.mjs @@ -0,0 +1,64 @@ +import { ConfigurationError } from "@pipedream/platform"; +import hubstaff from "../../hubstaff.app.mjs"; + +export default { + key: "hubstaff-create-task", + name: "Create Task", + description: "Creates a new task on your Hubstaff organization. [See the documentation](https://developer.hubstaff.com/docs/hubstaff_v2#!/tasks/postV2ProjectsProjectIdTasks)", + version: "0.0.1", + type: "action", + props: { + hubstaff, + organizationId: { + propDefinition: [ + hubstaff, + "organizationId", + ], + }, + projectId: { + propDefinition: [ + hubstaff, + "projectId", + ({ organizationId }) => ({ + organizationId, + }), + ], + }, + summary: { + propDefinition: [ + hubstaff, + "summary", + ], + }, + assigneeId: { + propDefinition: [ + hubstaff, + "userIds", + ({ projectId }) => ({ + projectId, + }), + ], + type: "string", + label: "User ID", + description: "Assignee user ID for this task.", + }, + }, + async run({ $ }) { + try { + const response = await this.hubstaff.createTask({ + $, + projectId: this.projectId, + data: { + summary: this.summary, + assignee_id: this.assigneeId, + }, + }); + + $.export("$summary", `Successfully created task with ID ${response.task.id}`); + return response; + } catch ({ message }) { + const { error } = JSON.parse(message); + throw new ConfigurationError(error); + } + }, +}; diff --git a/components/hubstaff/actions/list-tasks/list-tasks.mjs b/components/hubstaff/actions/list-tasks/list-tasks.mjs new file mode 100644 index 0000000000000..54903b2b8aaaa --- /dev/null +++ b/components/hubstaff/actions/list-tasks/list-tasks.mjs @@ -0,0 +1,71 @@ +import { INCLUDE_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import hubstaff from "../../hubstaff.app.mjs"; + +export default { + key: "hubstaff-list-tasks", + name: "List Tasks", + description: "Retrieves a list of all tasks from your Hubstaff organization. [See the documentation](https://developer.hubstaff.com/docs/hubstaff_v2#!/tasks/getV2OrganizationsOrganizationIdTasks)", + version: "0.0.1", + type: "action", + props: { + hubstaff, + organizationId: { + propDefinition: [ + hubstaff, + "organizationId", + ], + }, + projectId: { + propDefinition: [ + hubstaff, + "projectId", + ({ organizationId }) => ({ + organizationId, + }), + ], + type: "string[]", + optional: true, + }, + status: { + propDefinition: [ + hubstaff, + "status", + ], + type: "string[]", + optional: true, + }, + userIds: { + propDefinition: [ + hubstaff, + "userIds", + (c) => ({ + organizationId: c.organizationId, + }), + ], + optional: true, + }, + include: { + type: "string[]", + label: "Include", + description: "Specify related data to side load.", + options: INCLUDE_OPTIONS, + optional: true, + }, + }, + async run({ $ }) { + const response = await this.hubstaff.listAllTasks({ + $, + organizationId: this.organizationId, + params: { + include: parseObject(this.include), + project_ids: parseObject(this.projectId), + user_ids: parseObject(this.userIds), + status: parseObject(this.status), + }, + }); + + $.export("$summary", `Successfully retrieved ${response.tasks.length} task(s)`); + return response; + }, +}; diff --git a/components/hubstaff/actions/update-task/update-task.mjs b/components/hubstaff/actions/update-task/update-task.mjs new file mode 100644 index 0000000000000..f30b15df3e695 --- /dev/null +++ b/components/hubstaff/actions/update-task/update-task.mjs @@ -0,0 +1,95 @@ +import { ConfigurationError } from "@pipedream/platform"; +import hubstaff from "../../hubstaff.app.mjs"; + +export default { + key: "hubstaff-update-task", + name: "Update Task", + description: "Update a specific task within your Hubstaff organization. [See the documentation](https://developer.hubstaff.com/docs/hubstaff_v2#!/tasks/putV2TasksTaskId)", + version: "0.0.1", + type: "action", + props: { + hubstaff, + organizationId: { + propDefinition: [ + hubstaff, + "organizationId", + ], + }, + projectId: { + propDefinition: [ + hubstaff, + "projectId", + ({ organizationId }) => ({ + organizationId, + }), + ], + }, + taskId: { + propDefinition: [ + hubstaff, + "taskId", + ({ + organizationId, projectId, + }) => ({ + organizationId, + projectId, + }), + ], + }, + summary: { + propDefinition: [ + hubstaff, + "summary", + ], + }, + assigneeId: { + propDefinition: [ + hubstaff, + "userIds", + ({ projectId }) => ({ + projectId, + }), + ], + type: "string", + label: "User ID", + description: "Assignee user ID for this task.", + }, + status: { + propDefinition: [ + hubstaff, + "status", + ], + options: [ + "active", + "completed", + ], + optional: true, + }, + lockVersion: { + type: "integer", + label: "Lock Version", + description: "The lock version from the task fetch in order to update.", + default: 0, + }, + }, + async run({ $ }) { + try { + const response = await this.hubstaff.updateTask({ + $, + taskId: this.taskId, + data: { + summary: this.summary, + lock_version: this.lockVersion, + status: this.status, + assignee_id: this.assigneeId, + }, + }); + + $.export("$summary", `Successfully updated task with ID ${this.taskId}`); + return response; + } catch ({ message }) { + const { error } = JSON.parse(message); + throw new ConfigurationError(error); + } + }, +}; diff --git a/components/hubstaff/common/constants.mjs b/components/hubstaff/common/constants.mjs new file mode 100644 index 0000000000000..f9b5f37866dd2 --- /dev/null +++ b/components/hubstaff/common/constants.mjs @@ -0,0 +1,41 @@ +export const INCLUDE_OPTIONS = [ + { + label: "Users", + value: "users", + }, + { + label: "Projects", + value: "projects", + }, +]; + +export const STATUS_OPTIONS = [ + { + value: "active", + label: "Active", + }, + { + value: "completed", + label: "Completed", + }, + { + value: "deleted", + label: "Deleted", + }, + { + value: "archived", + label: "Archived", + }, + { + value: "archived_native_active", + label: "Archived Native Active", + }, + { + value: "archived_native_completed", + label: "Archived Native Completed", + }, + { + value: "archived_native_deleted", + label: "Archived Native Deleted", + }, +]; diff --git a/components/hubstaff/common/utils.mjs b/components/hubstaff/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/hubstaff/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/hubstaff/hubstaff.app.mjs b/components/hubstaff/hubstaff.app.mjs index 7aa0591bf8939..891862b9d9d0f 100644 --- a/components/hubstaff/hubstaff.app.mjs +++ b/components/hubstaff/hubstaff.app.mjs @@ -1,11 +1,264 @@ +import { axios } from "@pipedream/platform"; +import { STATUS_OPTIONS } from "./common/constants.mjs"; + export default { type: "app", app: "hubstaff", - propDefinitions: {}, + propDefinitions: { + organizationId: { + type: "string", + label: "Organization ID", + description: "The ID of the organization", + async options({ prevContext }) { + const params = {}; + if (prevContext.nextToken) { + params.page_start_id = prevContext.nextToken; + } + const { + organizations, pagination, + } = await this.listOrganizations({ + params, + }); + return { + options: organizations.map(({ + name: label, id: value, + }) => ({ + label, + value, + })), + context: { + nextToken: pagination + ? pagination.next_page_start_id + : false, + }, + }; + }, + }, + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project", + async options({ + prevContext, organizationId, + }) { + const params = {}; + if (prevContext.nextToken) { + params.page_start_id = prevContext.nextToken; + } + const { + projects, pagination, + } = await this.listProjects({ + params, + organizationId, + }); + + return { + options: projects.map(({ + name: label, id: value, + }) => ({ + label, + value, + })), + context: { + nextToken: pagination + ? pagination.next_page_start_id + : false, + }, + }; + }, + }, + taskId: { + type: "string", + label: "Task ID", + description: "The ID of the task", + async options({ + prevContext, organizationId, projectId, + }) { + const params = {}; + if (prevContext.nextToken) { + params.page_start_id = prevContext.nextToken; + } + const { + tasks, pagination, + } = await this.listTasks({ + params, + organizationId, + projectId, + }); + + return { + options: tasks.map(({ + id: value, summary: label, + }) => ({ + label, + value, + })), + context: { + nextToken: pagination + ? pagination.next_page_start_id + : false, + }, + }; + }, + }, + userIds: { + type: "string[]", + label: "User IDs", + description: "List of user IDs", + async options({ + prevContext, organizationId, projectId, + }) { + const params = { + include: [ + "users", + ], + }; + if (prevContext.nextToken) { + params.page_start_id = prevContext.nextToken; + } + const { + members, users, pagination, + } = await this.listUsers({ + params, + organizationId, + projectId, + }); + + return { + options: members.map(({ user_id: value }) => ({ + label: users.find((user) => user.id === value).name, + value, + })), + context: { + nextToken: pagination + ? pagination.next_page_start_id + : false, + }, + }; + }, + }, + status: { + type: "string", + label: "Status", + description: "The status of the task", + options: STATUS_OPTIONS, + }, + summary: { + type: "string", + label: "Summary", + description: "A brief summary of the task", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.hubstaff.com/v2"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listClients({ organizationId }) { + return this._makeRequest({ + path: `/organizations/${organizationId}/clients`, + }); + }, + listOrganizations() { + return this._makeRequest({ + path: "/organizations", + }); + }, + listProjects({ organizationId }) { + return this._makeRequest({ + path: `/organizations/${organizationId}/projects`, + }); + }, + listSchedules({ + organizationId, ...opts + }) { + return this._makeRequest({ + path: `/organizations/${organizationId}/attendance_schedules`, + ...opts, + }); + }, + listTasks({ projectId }) { + return this._makeRequest({ + path: `/projects/${projectId}/tasks`, + }); + }, + listUsers({ + organizationId, projectId, ...opts + }) { + return this._makeRequest({ + path: `${projectId + ? `/projects/${projectId}` + : `/organizations/${organizationId}`}/members`, + ...opts, + }); + }, + createTask({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/tasks`, + ...opts, + }); + }, + updateTask({ + taskId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/tasks/${taskId}`, + ...opts, + }); + }, + listAllTasks({ + organizationId, ...opts + }) { + return this._makeRequest({ + path: `/organizations/${organizationId}/tasks`, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, model, ...opts + }) { + let nextToken = false; + let page = 0; + + do { + params.page = ++page; + delete params.page_start_id; + if (nextToken) { + params.page_start_id = nextToken; + } + const { + pagination, [model]: data, + } = await fn({ + params, + ...opts, + }); + + for (const d of data) { + yield d; + } + + nextToken = pagination + ? pagination.next_page_start_id + : false; + + } while (nextToken); }, }, }; diff --git a/components/hubstaff/package.json b/components/hubstaff/package.json index 9c9a7f1b59692..097f6d98167bc 100644 --- a/components/hubstaff/package.json +++ b/components/hubstaff/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/hubstaff", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Hubstaff Components", "main": "hubstaff.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" } -} \ No newline at end of file +} + diff --git a/components/hubstaff/sources/common/base.mjs b/components/hubstaff/sources/common/base.mjs new file mode 100644 index 0000000000000..812eff6aa72fb --- /dev/null +++ b/components/hubstaff/sources/common/base.mjs @@ -0,0 +1,74 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import hubstaff from "../../hubstaff.app.mjs"; + +export default { + props: { + hubstaff, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + organizationId: { + propDefinition: [ + hubstaff, + "organizationId", + ], + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + getParams() { + return {}; + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + + const response = this.hubstaff.paginate({ + fn: this.getFunction(), + model: this.getModel(), + params: this.getParams(), + organizationId: this.organizationId, + }); + + let responseArray = []; + for await (const item of response) { + if (item.id > lastId) { + responseArray.push(item); + } + } + + responseArray.reverse(); + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item.created_at), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/hubstaff/sources/new-client/new-client.mjs b/components/hubstaff/sources/new-client/new-client.mjs new file mode 100644 index 0000000000000..70f876312b259 --- /dev/null +++ b/components/hubstaff/sources/new-client/new-client.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "hubstaff-new-client", + name: "New Client Created", + description: "Emit new event when a new client is created in Hubstaff.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getModel() { + return "clients"; + }, + getFunction() { + return this.hubstaff.listClients; + }, + getSummary(item) { + return `New Client: ${item.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/hubstaff/sources/new-client/test-event.mjs b/components/hubstaff/sources/new-client/test-event.mjs new file mode 100644 index 0000000000000..8ce1080390b23 --- /dev/null +++ b/components/hubstaff/sources/new-client/test-event.mjs @@ -0,0 +1,33 @@ +export default { + "id": 0, + "organization_id": 0, + "name": "string", + "emails": [ + "string" + ], + "phone": "string", + "address": "string", + "project_ids": [ + 0 + ], + "inherit_invoice_notes": true, + "invoice_notes": "string", + "inherit_net_terms": true, + "net_terms": "string", + "status": "string", + "created_at": "2024-07-31T13:50:20.664Z", + "updated_at": "2024-07-31T13:50:20.664Z", + "budget": { + "type": "string", + "rate": "string", + "cost": 0, + "hours": 0, + "start_date": "2024-07-31", + "alerts": { + "near_limit": 0 + }, + "recurrence": "string", + "include_non_billable": true + }, + "metadata": {} +} \ No newline at end of file diff --git a/components/hubstaff/sources/new-schedule/new-schedule.mjs b/components/hubstaff/sources/new-schedule/new-schedule.mjs new file mode 100644 index 0000000000000..3f6204ec9a26b --- /dev/null +++ b/components/hubstaff/sources/new-schedule/new-schedule.mjs @@ -0,0 +1,31 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "hubstaff-new-schedule", + name: "New Schedule Created", + description: "Emit new event when a schedule is created in Hubstaff.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getModel() { + return "attendance_schedules"; + }, + getFunction() { + return this.hubstaff.listSchedules; + }, + getParams() { + return { + "date[start]": "1970-01-01", + "date[stop]": ((d) => new Date(d.getFullYear() + 1000, d.getMonth(), d.getDate()))(new Date), + }; + }, + getSummary(item) { + return `New Schedule: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/hubstaff/sources/new-schedule/test-event.mjs b/components/hubstaff/sources/new-schedule/test-event.mjs new file mode 100644 index 0000000000000..5112222e6003b --- /dev/null +++ b/components/hubstaff/sources/new-schedule/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "id": 1234567, + "user_id": 1234567, + "organization_id": 1234567, + "created_at": "2024-08-01T14:56:16.366748Z", + "updated_at": "2024-08-01T14:56:16.366748Z", + "start_date": "2024-08-02", + "use_time_zone": "user", + "start_time": "09:00:00", + "duration": 28800, + "minimum_time": 27072, + "repeat_schedule": "never" +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2f4f5c9090e3..40e80f86cb85d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4315,7 +4315,10 @@ importers: specifiers: {} components/hubstaff: - specifiers: {} + specifiers: + '@pipedream/platform': ^3.0.0 + dependencies: + '@pipedream/platform': 3.0.0 components/hugging_face: specifiers: