diff --git a/components/remote/actions/create-expense/create-expense.mjs b/components/remote/actions/create-expense/create-expense.mjs new file mode 100644 index 0000000000000..52556bbe5014b --- /dev/null +++ b/components/remote/actions/create-expense/create-expense.mjs @@ -0,0 +1,138 @@ +import { getFileStreamAndMetadata } from "@pipedream/platform"; +import remote from "../../remote.app.mjs"; + +export default { + key: "remote-create-expense", + name: "Create Expense", + description: "Create an expense in Remote. [See the documentation](https://developer.remote.com/reference/post_create_expense)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + type: "action", + props: { + remote, + amount: { + type: "integer", + label: "Amount", + description: "The amount of the expense", + }, + currency: { + type: "string", + label: "Currency", + description: "The currency of the expense", + options: [ + "USD", + "EUR", + "CAD", + ], + }, + employmentId: { + propDefinition: [ + remote, + "employmentId", + ], + }, + expenseCategorySlug: { + propDefinition: [ + remote, + "expenseCategorySlug", + ({ employmentId }) => ({ + employmentId, + }), + ], + }, + expenseDate: { + type: "string", + label: "Expense Date", + description: "Date of the purchase, which must be in the past. **Format: YYYY-MM-DD**", + }, + receiptContentFilePath: { + type: "string", + label: "Receipt Content File Path", + description: "The file of the receipt. Provide either a file URL or a path to a file in the `/tmp` directory (for example, `/tmp/myFile.pdf`)", + }, + receiptFileName: { + type: "string", + label: "Receipt File Name", + description: "The file name", + }, + reviewedAt: { + type: "string", + label: "Reviewed At", + description: "The date and time that the expense was reviewed in ISO8601 format. If not provided, it defaults to the current datetime. **Format: YYYY-MM-DDTHH:MM:SS**", + }, + taxAmount: { + type: "integer", + label: "Tax Amount", + description: "The tax amount of the expense", + optional: true, + }, + timezone: { + type: "string", + label: "Timezone", + description: "The timezone of the expense. [TZ identifier](https://www.iana.org/time-zones)", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "The title of the expense", + }, + syncDir: { + type: "dir", + accessMode: "read", + sync: true, + }, + }, + methods: { + async streamToBase64(stream) { + return new Promise((resolve, reject) => { + const chunks = []; + + stream.on("data", (chunk) => { + chunks.push(chunk); + }); + + stream.on("end", () => { + const buffer = Buffer.concat(chunks); + resolve(buffer.toString("base64")); + }); + + stream.on("error", (err) => { + reject(err); + }); + }); + }, + }, + async run({ $ }) { + const { + stream, metadata, + } = await getFileStreamAndMetadata(this.receiptContentFilePath); + const base64String = await this.streamToBase64(stream); + + const response = await this.remote.createExpense({ + $, + data: { + amount: this.amount, + currency: this.currency, + employment_id: this.employmentId, + expense_date: this.expenseDate, + title: this.title, + receipt: { + content: `data:${metadata.contentType};base64,${base64String}`, + name: this.receiptFileName, + }, + expense_category_slug: this.expenseCategorySlug, + reviewed_at: this.reviewedAt, + tax_amount: this.taxAmount, + timezone: this.timezone, + }, + }); + + $.export("$summary", `Successfully created expense with ID: ${response?.data?.expense?.id}`); + return response; + }, +}; diff --git a/components/remote/actions/list-employments/list-employments.mjs b/components/remote/actions/list-employments/list-employments.mjs new file mode 100644 index 0000000000000..522824ccf54b1 --- /dev/null +++ b/components/remote/actions/list-employments/list-employments.mjs @@ -0,0 +1,72 @@ +import remote from "../../remote.app.mjs"; + +export default { + key: "remote-list-employments", + name: "List Employments", + description: "List employments in Remote. [See the documentation](https://developer.remote.com/reference/get_index_employment)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: true, + }, + type: "action", + props: { + remote, + email: { + type: "string", + label: "Email", + description: "Filters the results by employments whose login email matches the value", + optional: true, + }, + employmentType: { + type: "string", + label: "Employment Type", + description: "Filters the results by employments whose employment product type matches the value", + optional: true, + options: [ + "contractor", + "direct_employee", + "employee", + ], + }, + employmentModel: { + type: "string", + label: "Employment Model", + description: "Filters the results by employments whose employment model matches the value", + optional: true, + options: [ + "global_payroll", + "peo", + "eor", + ], + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of results to return", + optional: true, + }, + }, + async run({ $ }) { + const response = this.remote.paginate({ + $, + dataKey: "employments", + fn: this.remote.listEmployments, + params: { + email: this.email, + employment_type: this.employmentType, + employment_model: this.employmentModel, + }, + maxResults: this.maxResults, + }); + + const responseArray = []; + for await (const employment of response) { + responseArray.push(employment); + } + + $.export("$summary", `Successfully listed ${responseArray.length} employments`); + return responseArray; + }, +}; diff --git a/components/remote/actions/show-timeoff-balance/show-timeoff-balance.mjs b/components/remote/actions/show-timeoff-balance/show-timeoff-balance.mjs new file mode 100644 index 0000000000000..1ee96f437318c --- /dev/null +++ b/components/remote/actions/show-timeoff-balance/show-timeoff-balance.mjs @@ -0,0 +1,32 @@ +import remote from "../../remote.app.mjs"; + +export default { + key: "remote-show-timeoff-balance", + name: "Show Time Off Balance", + description: "Show the time off balance for an employment in Remote. [See the documentation](https://developer.remote.com/reference/get_show_timeoff_balance)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: true, + }, + type: "action", + props: { + remote, + employmentId: { + propDefinition: [ + remote, + "employmentId", + ], + }, + }, + async run({ $ }) { + const response = await this.remote.showTimeoffBalance({ + $, + employmentId: this.employmentId, + }); + + $.export("$summary", `Successfully retrieved time off balance for employment with ID: ${this.employmentId}`); + return response; + }, +}; diff --git a/components/remote/package.json b/components/remote/package.json index ab10145d55852..84d2155079018 100644 --- a/components/remote/package.json +++ b/components/remote/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/remote", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Remote Components", "main": "remote.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.1.1" } } diff --git a/components/remote/remote.app.mjs b/components/remote/remote.app.mjs index acd34adbc71d7..daff91a36df65 100644 --- a/components/remote/remote.app.mjs +++ b/components/remote/remote.app.mjs @@ -1,11 +1,139 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "remote", - propDefinitions: {}, + propDefinitions: { + employmentId: { + type: "string", + label: "Employment ID", + description: "The ID of the employment to create the expense for", + async options({ page }) { + const { data: { employments } } = await this.listEmployments({ + params: { + page: page + 1, + }, + }); + + return employments.map(({ + id, full_name: name, personal_email: email, + }) => ({ + label: `${name} (${email})`, + value: id, + })); + }, + }, + expenseCategorySlug: { + type: "string", + label: "Expense Category Slug", + description: "The slug of the expense category to create the expense for", + async options({ employmentId }) { + const { data } = await this.listExpenseCategories({ + params: { + employment_id: employmentId, + }, + }); + return data.map(({ + slug, title, + }) => ({ + label: title, + value: slug, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://gateway.${this.$auth.environment}.com/v1`; + }, + _getHeaders() { + return { + "Authorization": `Bearer ${this.$auth.api_token}`, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._getHeaders(), + ...opts, + }); + }, + createExpense(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/expenses", + ...args, + }); + }, + listEmployments(args = {}) { + return this._makeRequest({ + path: "/employments", + ...args, + }); + }, + listExpenseCategories(args = {}) { + return this._makeRequest({ + path: "/expenses/categories", + ...args, + }); + }, + showTimeoffBalance({ + employmentId, ...args + }) { + return this._makeRequest({ + path: `/timeoff-balances/${employmentId}`, + ...args, + }); + }, + createHook(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook-callbacks", + ...args, + }); + }, + deleteHook(hookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhook-callbacks/${hookId}`, + }); + }, + async *paginate({ + fn, params = {}, dataKey = "data", maxResults = null, ...args + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { + data: { + [dataKey]: data, + current_page, + total_pages, + }, + } = await fn({ + params, + ...args, + }); + + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = current_page < total_pages; + + } while (hasMore); }, }, -}; \ No newline at end of file +}; diff --git a/components/remote/sources/common/base.mjs b/components/remote/sources/common/base.mjs new file mode 100644 index 0000000000000..258379b2aaf39 --- /dev/null +++ b/components/remote/sources/common/base.mjs @@ -0,0 +1,43 @@ +import remote from "../../remote.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + props: { + remote, + db: "$.service.db", + http: "$.interface.http", + }, + methods: { + _setWebhookId(id) { + this.db.set("webhookId", id); + }, + _getWebhookId() { + return this.db.get("webhookId"); + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + }, + hooks: { + async activate() { + const { data: { webhook_callback: { id } } } = await this.remote.createHook({ + data: { + url: this.http.endpoint, + subscribed_events: this.getEvent(), + }, + }); + this._setWebhookId(id); + }, + async deactivate() { + const webhookId = this._getWebhookId(); + await this.remote.deleteHook(webhookId); + }, + }, + async run({ body }) { + const ts = Date.now(); + this.$emit(body, this.generateMeta({ + body, + ts, + })); + }, +}; diff --git a/components/remote/sources/new-contract-amendment-done-instant/new-contract-amendment-done-instant.mjs b/components/remote/sources/new-contract-amendment-done-instant/new-contract-amendment-done-instant.mjs new file mode 100644 index 0000000000000..d000a11a55f91 --- /dev/null +++ b/components/remote/sources/new-contract-amendment-done-instant/new-contract-amendment-done-instant.mjs @@ -0,0 +1,31 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "remote-new-contract-amendment-done-instant", + name: "New Contract Amendment Done (Instant)", + description: "Emit new event when a contract amendment is done. [See the documentation](https://developer.remote.com/reference/contract_amendmentdone)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return [ + "contract_amendment.done", + ]; + }, + generateMeta({ + body, + ts, + }) { + return { + id: `${body.employment_request_id}-${ts}`, + summary: `Contract amendment for employment with ID ${body.employment_request_id} is done`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/remote/sources/new-contract-amendment-done-instant/test-event.mjs b/components/remote/sources/new-contract-amendment-done-instant/test-event.mjs new file mode 100644 index 0000000000000..87e1290e8a58d --- /dev/null +++ b/components/remote/sources/new-contract-amendment-done-instant/test-event.mjs @@ -0,0 +1,5 @@ +export default { + "employment_id": "2614f814-b08e-4c8e-8c4d-ddbcc4692d99", + "employment_request_id": "129d02bc-dd6a-11ed-ac99-cb057df06a33", + "event_type": "contract_amendment.done", +} \ No newline at end of file diff --git a/components/remote/sources/new-custom-field-value-updated-instant/new-custom-field-value-updated-instant.mjs b/components/remote/sources/new-custom-field-value-updated-instant/new-custom-field-value-updated-instant.mjs new file mode 100644 index 0000000000000..113eb3be33fb3 --- /dev/null +++ b/components/remote/sources/new-custom-field-value-updated-instant/new-custom-field-value-updated-instant.mjs @@ -0,0 +1,31 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "remote-new-custom-field-value-updated-instant", + name: "New Custom Field Value Updated (Instant)", + description: "Emit new event when a custom field is updated. [See the documentation](https://developer.remote.com/reference/custom_fieldvalue_updated)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return [ + "custom_field.value_updated", + ]; + }, + generateMeta({ + body, + ts, + }) { + return { + id: `${body.custom_field_id}-${ts}`, + summary: `Custom field with ID ${body.custom_field_id} has been updated`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/remote/sources/new-custom-field-value-updated-instant/test-event.mjs b/components/remote/sources/new-custom-field-value-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..54deedb923b0a --- /dev/null +++ b/components/remote/sources/new-custom-field-value-updated-instant/test-event.mjs @@ -0,0 +1,5 @@ +export default { + "custom_field_id": "2614f814-b08e-4c8e-8c4d-ddbcc4692d99", + "employment_id": "129d02bc-dd6a-11ed-ac99-cb057df06a33", + "event_type": "custom_field.value_updated", +} \ No newline at end of file diff --git a/components/remote/sources/new-employment-onboarding-started-instant/new-employment-onboarding-started-instant.mjs b/components/remote/sources/new-employment-onboarding-started-instant/new-employment-onboarding-started-instant.mjs new file mode 100644 index 0000000000000..05ba6c3d15cc7 --- /dev/null +++ b/components/remote/sources/new-employment-onboarding-started-instant/new-employment-onboarding-started-instant.mjs @@ -0,0 +1,31 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "remote-new-employment-onboarding-started-instant", + name: "New Employment Onboarding Started (Instant)", + description: "Emit new event when an employment onboarding is started. [See the documentation](https://developer.remote.com/reference/employmentonboardingstarted)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return [ + "employment.onboarding.started", + ]; + }, + generateMeta({ + body, + ts, + }) { + return { + id: `${body.employment_id}-${ts}`, + summary: `Onboarding for employment with ID ${body.employment_id} has been started`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/remote/sources/new-employment-onboarding-started-instant/test-event.mjs b/components/remote/sources/new-employment-onboarding-started-instant/test-event.mjs new file mode 100644 index 0000000000000..ab03fdce3c40b --- /dev/null +++ b/components/remote/sources/new-employment-onboarding-started-instant/test-event.mjs @@ -0,0 +1,4 @@ +export default { + "employment_id": "2614f814-b08e-4c8e-8c4d-ddbcc4692d99", + "event_type": "employment.onboarding.started", +} \ No newline at end of file diff --git a/components/upsales/upsales.app.mjs b/components/upsales/upsales.app.mjs index 25742c2e6da57..a25cb38c3794c 100644 --- a/components/upsales/upsales.app.mjs +++ b/components/upsales/upsales.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/xero_payroll/xero_payroll.app.mjs b/components/xero_payroll/xero_payroll.app.mjs index 43ded58f0da08..6636a5fb991fb 100644 --- a/components/xero_payroll/xero_payroll.app.mjs +++ b/components/xero_payroll/xero_payroll.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 5c38d6e1bf2ca..454db34010702 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12381,7 +12381,11 @@ importers: components/remarkety: {} - components/remote: {} + components/remote: + dependencies: + '@pipedream/platform': + specifier: ^3.1.1 + version: 3.1.1 components/remote_retrieval: dependencies: