diff --git a/components/habitify/app/habitify.app.mjs b/components/habitify/app/habitify.app.mjs deleted file mode 100644 index a8728172ea2fb..0000000000000 --- a/components/habitify/app/habitify.app.mjs +++ /dev/null @@ -1,32 +0,0 @@ -import { axios } from "@pipedream/platform"; - -export default { - type: "app", - app: "habitify", - propDefinitions: {}, - methods: { - _apiKey() { - return this.$auth.api_key; - }, - _apiUrl() { - return "https://api.habitify.me"; - }, - async _makeRequest({ - $ = this, path, ...args - }) { - return axios($, { - url: `${this._apiUrl()}${path}`, - headers: { - Authorization: this._apiKey(), - }, - ...args, - }); - }, - async getHabits(args = {}) { - return this._makeRequest({ - path: "/habits", - ...args, - }); - }, - }, -}; diff --git a/components/habitify/habitify.app.mjs b/components/habitify/habitify.app.mjs new file mode 100644 index 0000000000000..dbc6f9871acdb --- /dev/null +++ b/components/habitify/habitify.app.mjs @@ -0,0 +1,63 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "habitify", + propDefinitions: { + habitIds: { + type: "string[]", + label: "Habit IDs", + description: "The IDs of the habits to watch", + async options() { + const { data } = await this.getHabits(); + return data?.map(({ + id, name, + }) => ({ + label: name, + value: id, + })) || []; + }, + }, + }, + methods: { + _apiKey() { + return this.$auth.api_key; + }, + _apiUrl() { + return "https://api.habitify.me"; + }, + async _makeRequest({ + $ = this, path, ...args + }) { + return axios($, { + url: `${this._apiUrl()}${path}`, + headers: { + Authorization: this._apiKey(), + }, + ...args, + }); + }, + getHabits(args = {}) { + return this._makeRequest({ + path: "/habits", + ...args, + }); + }, + getHabitStatus({ + habitId, ...args + }) { + return this._makeRequest({ + path: `/status/${habitId}`, + ...args, + }); + }, + getHabitLogs({ + habitId, ...args + }) { + return this._makeRequest({ + path: `/logs/${habitId}`, + ...args, + }); + }, + }, +}; diff --git a/components/habitify/package.json b/components/habitify/package.json index 53d29d3be1a4c..1514313d4abea 100644 --- a/components/habitify/package.json +++ b/components/habitify/package.json @@ -1,8 +1,8 @@ { "name": "@pipedream/habitify", - "version": "0.0.4", + "version": "0.1.0", "description": "Pipedream Habitify Components", - "main": "app/habitify.app.mjs", + "main": "habitify.app.mjs", "keywords": [ "pipedream", "habitify" @@ -13,6 +13,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.6.8" + "@pipedream/platform": "^3.1.1" } } diff --git a/components/habitify/sources/common/base-polling.mjs b/components/habitify/sources/common/base-polling.mjs new file mode 100644 index 0000000000000..bdd20100a5c88 --- /dev/null +++ b/components/habitify/sources/common/base-polling.mjs @@ -0,0 +1,42 @@ +import habitify from "../../habitify.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + habitify, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + getCurrentDateTime() { + return new Date().toISOString() + .replace("Z", this.getTimeZoneOffset()); + }, + getTimeZoneOffset() { + const offset = new Date().getTimezoneOffset(); + const sign = offset > 0 + ? "-" + : "+"; + const abs = Math.abs(offset); + const hours = String(Math.floor(abs / 60)).padStart(2, "0"); + const minutes = String(abs % 60).padStart(2, "0"); + return `${sign}${hours}:${minutes}`; + }, + convertToUTCOffset(dateString) { + const date = new Date(dateString); + const iso = + date.getUTCFullYear() + + "-" + String(date.getUTCMonth() + 1).padStart(2, "0") + + "-" + String(date.getUTCDate()).padStart(2, "0") + + "T" + String(date.getUTCHours()).padStart(2, "0") + + ":" + String(date.getUTCMinutes()).padStart(2, "0") + + ":" + String(date.getUTCSeconds()).padStart(2, "0"); + return iso + "+00:00"; + }, + }, +}; diff --git a/components/habitify/sources/habit-logged/habit-logged.mjs b/components/habitify/sources/habit-logged/habit-logged.mjs new file mode 100644 index 0000000000000..8ef6bb0ab4570 --- /dev/null +++ b/components/habitify/sources/habit-logged/habit-logged.mjs @@ -0,0 +1,65 @@ +import common from "../common/base-polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "habitify-habit-logged", + name: "Habit Logged", + description: "Emit new event when a new log is created for the selected habit(s). [See the documentation](https://docs.habitify.me/core-resources/habits/logs)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + habitIds: { + propDefinition: [ + common.props.habitify, + "habitIds", + ], + }, + }, + methods: { + ...common.methods, + _getLastTs() { + return this.db.get("lastTs") || this.convertToUTCOffset(this.getCurrentDateTime()); + }, + _setLastTs(ts) { + this.db.set("lastTs", ts); + }, + generateMeta(log) { + return { + id: log.id, + summary: `New log with ID ${log.id} created for habit ${log.habit_id}`, + ts: Date.parse(log.created_date), + }; + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const params = { + from: lastTs, + to: this.convertToUTCOffset(this.getCurrentDateTime()), + }; + const logs = []; + for (const habitId of this.habitIds) { + const { data } = await this.habitify.getHabitLogs({ + habitId, + params, + }); + if (!data.length) { + continue; + } + logs.push(...data); + if (Date.parse(data[data.length - 1].created_date) > Date.parse(maxTs)) { + maxTs = this.convertToUTCOffset(data[data.length - 1].created_date); + } + } + this._setLastTs(maxTs); + logs.sort((a, b) => Date.parse(a.created_date) - Date.parse(b.created_date)); + logs.forEach((log) => { + this.$emit(log, this.generateMeta(log)); + }); + }, + sampleEmit, +}; diff --git a/components/habitify/sources/habit-logged/test-event.mjs b/components/habitify/sources/habit-logged/test-event.mjs new file mode 100644 index 0000000000000..5509289092753 --- /dev/null +++ b/components/habitify/sources/habit-logged/test-event.mjs @@ -0,0 +1,7 @@ +export default { + "id": "-Og94QB1QHNBaFW1Q12v", + "value": 1, + "created_date": "2025-12-10T21:05:52.838Z", + "unit_type": "rep", + "habit_id": "C0D4CD24-EF3E-4EAB-9D88-17BBDE801FC1" +} \ No newline at end of file diff --git a/components/habitify/sources/habit-status-updated/habit-status-updated.mjs b/components/habitify/sources/habit-status-updated/habit-status-updated.mjs new file mode 100644 index 0000000000000..2f56642482e66 --- /dev/null +++ b/components/habitify/sources/habit-status-updated/habit-status-updated.mjs @@ -0,0 +1,87 @@ +import common from "../common/base-polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "habitify-habit-status-updated", + name: "Habit Status Updated", + description: "Emit new event when the status of a habit is updated. [See the documentation](https://docs.habitify.me/core-resources/habits/status)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + habitIds: { + propDefinition: [ + common.props.habitify, + "habitIds", + ], + }, + statuses: { + type: "string[]", + label: "Statuses", + description: "The statuses to watch for. [See the documentation](https://docs.habitify.me/core-resources/habits/status)", + options: [ + "in_progress", + "completed", + "skipped", + "failed", + ], + optional: true, + }, + }, + hooks: { + async deploy() { + const previousStatuses = {}; + for (const habitId of this.habitIds) { + const { data } = await this.habitify.getHabitStatus({ + habitId, + params: { + target_date: this.convertToUTCOffset(this.getCurrentDateTime()), + }, + }); + previousStatuses[habitId] = data.status; + } + this._setPreviousStatuses(previousStatuses); + }, + }, + methods: { + ...common.methods, + _getPreviousStatuses() { + return this.db.get("previousStatuses") || {}; + }, + _setPreviousStatuses(statuses) { + this.db.set("previousStatuses", statuses); + }, + generateMeta(status) { + return { + id: `${status.habit_id}-${status.progress.reference_date}`, + summary: `New Status ${status.status} for habit ${status.habit_id}`, + ts: Date.parse(status.progress.reference_date), + }; + }, + }, + async run() { + const previousStatuses = this._getPreviousStatuses(); + const currentStatuses = {}; + const targetDate = this.convertToUTCOffset(this.getCurrentDateTime()); + for (const habitId of this.habitIds) { + const { data } = await this.habitify.getHabitStatus({ + habitId, + params: { + target_date: targetDate, + }, + }); + currentStatuses[habitId] = data.status; + if ( + previousStatuses[habitId] !== data.status + && (!this.statuses || this.statuses.includes(data.status)) + ) { + data.habit_id = habitId; + this.$emit(data, this.generateMeta(data)); + } + } + this._setPreviousStatuses(currentStatuses); + }, + sampleEmit, +}; diff --git a/components/habitify/sources/habit-status-updated/test-event.mjs b/components/habitify/sources/habit-status-updated/test-event.mjs new file mode 100644 index 0000000000000..300dee10e0a92 --- /dev/null +++ b/components/habitify/sources/habit-status-updated/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "status": "completed", + "progress": { + "current_value": 8, + "target_value": 8, + "unit_type": "rep", + "periodicity": "daily", + "reference_date": "2025-12-10T00:00:00.000Z" + } +} \ No newline at end of file diff --git a/components/habitify/sources/new-habit-created/new-habit-created.mjs b/components/habitify/sources/new-habit-created/new-habit-created.mjs index dd71b80fcc798..467a0d3c72adf 100644 --- a/components/habitify/sources/new-habit-created/new-habit-created.mjs +++ b/components/habitify/sources/new-habit-created/new-habit-created.mjs @@ -1,23 +1,14 @@ -import habitify from "../../app/habitify.app.mjs"; -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import common from "../common/base-polling.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, name: "New Habit Created", - version: "0.0.1", + version: "0.0.2", key: "habitify-new-habit-created", - description: "Emit new event on each created habit.", + description: "Emit new event on each created habit. [See the documentation](https://docs.habitify.me/core-resources/habits#list-habits)", type: "source", dedupe: "unique", - props: { - habitify, - db: "$.service.db", - timer: { - type: "$.interface.timer", - static: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - }, methods: { emitEvent(data) { this.$emit(data, { @@ -32,4 +23,5 @@ export default { habits.reverse().forEach(this.emitEvent); }, + sampleEmit, }; diff --git a/components/habitify/sources/new-habit-created/test-event.mjs b/components/habitify/sources/new-habit-created/test-event.mjs new file mode 100644 index 0000000000000..18abab797273d --- /dev/null +++ b/components/habitify/sources/new-habit-created/test-event.mjs @@ -0,0 +1,29 @@ +export default { + "id": "C0D4CD24-EF3E-4EAB-9D88-17BBDE801FC1", + "name": "Drink Water", + "is_archived": false, + "start_date": "2025-12-10T19:57:16.566Z", + "time_of_day": [ + "any_time" + ], + "goal": { + "unit_type": "rep", + "value": 8, + "periodicity": "daily" + }, + "goal_history_items": [ + { + "unit_type": "rep", + "value": 8, + "periodicity": "daily" + } + ], + "log_method": "manual", + "recurrence": "DTSTART:20251210T195716Z\nRRULE:FREQ=DAILY", + "remind": [ + "6:30" + ], + "area": null, + "created_date": "2025-12-10T19:57:16.566Z", + "priority": 0 +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff169f8733652..8e18de55f85db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5107,8 +5107,7 @@ importers: specifier: ^3.1.1 version: 3.1.1 - components/fashn: - specifiers: {} + components/fashn: {} components/fastfield_mobile_forms: {} @@ -6672,8 +6671,8 @@ importers: components/habitify: dependencies: '@pipedream/platform': - specifier: ^1.6.8 - version: 1.6.8 + specifier: ^3.1.1 + version: 3.1.1 components/hacker_news: dependencies: @@ -7710,8 +7709,7 @@ importers: specifier: ^3.1.1 version: 3.1.1 - components/jobsoid_careers_portal: - specifiers: {} + components/jobsoid_careers_portal: {} components/joggai: dependencies: @@ -15859,8 +15857,7 @@ importers: specifier: ^13.0.0 version: 13.0.0 - components/upsales: - specifiers: {} + components/upsales: {} components/upstash_redis: dependencies: @@ -16798,8 +16795,7 @@ importers: specifier: ^3.1.1 version: 3.1.1 - components/xero_payroll: - specifiers: {} + components/xero_payroll: {} components/xola: dependencies: