Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions components/tmetric/actions/create-time-entry/create-time-entry.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import app from "../../tmetric.app.mjs";

export default {
key: "tmetric-create-time-entry",
name: "Create Time Entry",
description: "Create a new Time Entry. [See the documentation](https://app.tmetric.com/api-docs/?_gl=1*n76hcy*_gcl_aw*R0NMLjE3NjQwOTU2MDguQ2owS0NRaUF4SlhKQmhEX0FSSXNBSF9KR2pncU8zZzgxcHp3VUhQWGdjazEyUWFTaThXeE00ZTBUZ1hvak5ma1AzeG10a1pGYWJuek8wOGFBbC1KRUFMd193Y0I.*_gcl_au*MjU2MzU4NTI3LjE3NjQwOTU2MDg.*_ga*ODE4ODUyMDE2LjE3NjQwOTU2MDM.*_ga_66EFKVJKC5*czE3NjQwOTU2MDMkbzEkZzEkdDE3NjQwOTYyODkkajQzJGwwJGgw#/Time%20Entries/post-accounts-accountId-timeentries)",
version: "0.0.1",
annotations: {
destructiveHint: false,
openWorldHint: true,
readOnlyHint: false,
},
type: "action",
props: {
app,
accountId: {
propDefinition: [
app,
"accountId",
],
},
startTime: {
propDefinition: [
app,
"startTime",
],
},
endTime: {
propDefinition: [
app,
"endTime",
],
},
note: {
propDefinition: [
app,
"note",
],
},
isBillable: {
propDefinition: [
app,
"isBillable",
],
},
isInvoiced: {
propDefinition: [
app,
"isInvoiced",
],
},
},
async run({ $ }) {
const response = await this.app.createTimeEntry({
$,
accountId: this.accountId,
data: {
startTime: this.startTime ?? "",
endTime: this.endTime ?? "",
note: this.note,
isBillable: this.isBillable,
isInvoiced: this.isInvoiced,
},
Comment on lines +58 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's find and examine the create-time-entry.mjs file
fd -t f "create-time-entry.mjs" | head -20

Repository: PipedreamHQ/pipedream

Length of output: 464


🏁 Script executed:

#!/bin/bash
# Read the tmetric create-time-entry.mjs file
cat -n components/tmetric/actions/create-time-entry/create-time-entry.mjs

Repository: PipedreamHQ/pipedream

Length of output: 2263


🏁 Script executed:

#!/bin/bash
# Find and read tmetric app file
fd -t f "tmetric.app.mjs"

Repository: PipedreamHQ/pipedream

Length of output: 99


🏁 Script executed:

#!/bin/bash
# Read the tmetric app file to see propDefinitions and createTimeEntry
cat -n components/tmetric/tmetric.app.mjs

Repository: PipedreamHQ/pipedream

Length of output: 4184


🏁 Script executed:

#!/bin/bash
# Search for modify-time-entry action in tmetric
fd -t f "modify-time-entry.mjs" components/tmetric/

Repository: PipedreamHQ/pipedream

Length of output: 131


🏁 Script executed:

#!/bin/bash
# Read modify-time-entry.mjs to check for same pattern
cat -n components/tmetric/actions/modify-time-entry/modify-time-entry.mjs

Repository: PipedreamHQ/pipedream

Length of output: 2555


Avoid coercing missing startTime / endTime to empty strings

Using this.startTime ?? "" and this.endTime ?? "" forces empty strings into the payload when the user leaves these props blank. This conflicts with the API documentation in propDefinitions ("leave null to start the timer at the current moment" and "leave null to keep the timer running indefinitely") and prevents relying on the platform client's behavior of omitting undefined fields.

You can pass the values directly and let undefined be dropped from the request:

-      data: {
-        startTime: this.startTime ?? "",
-        endTime: this.endTime ?? "",
-        note: this.note,
-        isBillable: this.isBillable,
-        isInvoiced: this.isInvoiced,
-      },
+      data: {
+        startTime: this.startTime,
+        endTime: this.endTime,
+        note: this.note,
+        isBillable: this.isBillable,
+        isInvoiced: this.isInvoiced,
+      },

Apply this same adjustment to the modify-time-entry action (lines 68-69) for consistency.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
startTime: this.startTime ?? "",
endTime: this.endTime ?? "",
note: this.note,
isBillable: this.isBillable,
isInvoiced: this.isInvoiced,
},
startTime: this.startTime,
endTime: this.endTime,
note: this.note,
isBillable: this.isBillable,
isInvoiced: this.isInvoiced,
},
🤖 Prompt for AI Agents
In components/tmetric/actions/create-time-entry/create-time-entry.mjs around
lines 58 to 63, the code coerces missing startTime/endTime to empty strings
using "this.startTime ?? ''" and "this.endTime ?? ''"; change these to pass the
properties directly (this.startTime and this.endTime) so undefined values are
preserved and can be omitted by the platform client; also make the same
adjustment in the modify-time-entry action at lines 68-69 to keep behavior
consistent.

});
$.export("$summary", "Successfully created time entry with ID: " + response[0].id);
return response;
Comment on lines +65 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Defensively handle the createTimeEntry response shape when building the summary

The summary assumes createTimeEntry returns an array (response[0].id). If the API instead returns a single object, this will produce undefined in the summary.

You can support both shapes with a small adjustment:

-    $.export("$summary", "Successfully created time entry with ID: " + response[0].id);
-    return response;
+    const timeEntry = Array.isArray(response) ? response[0] : response;
+    $.export("$summary", `Successfully created time entry with ID: ${timeEntry.id}`);
+    return response;

This keeps the summary robust even if the API or client behavior changes.

🤖 Prompt for AI Agents
In components/tmetric/actions/create-time-entry/create-time-entry.mjs around
lines 65-66, the summary currently assumes response is an array and uses
response[0].id which can be undefined if the API returns a single object; update
the code to normalize the response (check Array.isArray(response) and pick
response[0] when array, otherwise use response), defensively read the id with
optional chaining (e.g. entry?.id) and fall back to a sensible placeholder like
'unknown' before building the export summary string so the summary works for
both array and single-object responses.

},
};
43 changes: 43 additions & 0 deletions components/tmetric/actions/delete-time-entry/delete-time-entry.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import app from "../../tmetric.app.mjs";

export default {
key: "tmetric-delete-time-entry",
name: "Delete Time Entry",
description: "Delete the specified Time Entry. [See the documentation](https://app.tmetric.com/api-docs/?_gl=1*n76hcy*_gcl_aw*R0NMLjE3NjQwOTU2MDguQ2owS0NRaUF4SlhKQmhEX0FSSXNBSF9KR2pncU8zZzgxcHp3VUhQWGdjazEyUWFTaThXeE00ZTBUZ1hvak5ma1AzeG10a1pGYWJuek8wOGFBbC1KRUFMd193Y0I.*_gcl_au*MjU2MzU4NTI3LjE3NjQwOTU2MDg.*_ga*ODE4ODUyMDE2LjE3NjQwOTU2MDM.*_ga_66EFKVJKC5*czE3NjQwOTU2MDMkbzEkZzEkdDE3NjQwOTYyODkkajQzJGwwJGgw#/Time%20Entries/delete-accounts-accountId-timeentries-timeEntryId)",
version: "0.0.1",
annotations: {
destructiveHint: true,
openWorldHint: true,
readOnlyHint: false,
},
type: "action",
props: {
app,
accountId: {
propDefinition: [
app,
"accountId",
],
},
timeEntryId: {
propDefinition: [
app,
"timeEntryId",
(c) => ({
accountId: c.accountId,
}),
],
},
},
async run({ $ }) {
const response = await this.app.deleteTimeEntry({
$,
data: {
accountId: this.accountId,
timeEntryId: this.timeEntryId,
},
});
Comment on lines +32 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix deleteTimeEntry call: IDs are not passed where the app method expects them

deleteTimeEntry in tmetric.app.mjs destructures accountId and timeEntryId from the top‑level argument, but this action passes them inside data, so both IDs are undefined and the URL becomes /accounts/undefined/timeentries/undefined.

You should pass the IDs at the top level (and you don't need them in the body):

-    const response = await this.app.deleteTimeEntry({
-      $,
-      data: {
-        accountId: this.accountId,
-        timeEntryId: this.timeEntryId,
-      },
-    });
+    const response = await this.app.deleteTimeEntry({
+      $,
+      accountId: this.accountId,
+      timeEntryId: this.timeEntryId,
+    });
🤖 Prompt for AI Agents
In components/tmetric/actions/delete-time-entry/delete-time-entry.mjs around
lines 32 to 39, the call to this.app.deleteTimeEntry incorrectly nests accountId
and timeEntryId inside a data object so the app method receives undefined IDs;
move accountId and timeEntryId to the top-level of the argument object (e.g.
pass { $, accountId: this.accountId, timeEntryId: this.timeEntryId }) and remove
them from the body/data payload since the endpoint derives IDs from the path.

$.export("$summary", "Successfully deleted time entry with ID: " + this.timeEntryId);
return response;
},
};
78 changes: 78 additions & 0 deletions components/tmetric/actions/modify-time-entry/modify-time-entry.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import app from "../../tmetric.app.mjs";

export default {
key: "tmetric-modify-time-entry",
name: "Modify Time Entry",
description: "Modify the specified Time Entry. [See the documentation](https://app.tmetric.com/api-docs/?_gl=1*n76hcy*_gcl_aw*R0NMLjE3NjQwOTU2MDguQ2owS0NRaUF4SlhKQmhEX0FSSXNBSF9KR2pncU8zZzgxcHp3VUhQWGdjazEyUWFTaThXeE00ZTBUZ1hvak5ma1AzeG10a1pGYWJuek8wOGFBbC1KRUFMd193Y0I.*_gcl_au*MjU2MzU4NTI3LjE3NjQwOTU2MDg.*_ga*ODE4ODUyMDE2LjE3NjQwOTU2MDM.*_ga_66EFKVJKC5*czE3NjQwOTU2MDMkbzEkZzEkdDE3NjQwOTYyODkkajQzJGwwJGgw#/Time%20Entries/put-accounts-accountId-timeentries-timeEntryId)",
version: "0.0.1",
annotations: {
destructiveHint: false,
openWorldHint: true,
readOnlyHint: false,
},
type: "action",
props: {
app,
accountId: {
propDefinition: [
app,
"accountId",
],
},
timeEntryId: {
propDefinition: [
app,
"timeEntryId",
(c) => ({
accountId: c.accountId,
}),
],
},
startTime: {
propDefinition: [
app,
"startTime",
],
},
endTime: {
propDefinition: [
app,
"endTime",
],
},
note: {
propDefinition: [
app,
"note",
],
},
isBillable: {
propDefinition: [
app,
"isBillable",
],
},
isInvoiced: {
propDefinition: [
app,
"isInvoiced",
],
},
},
async run({ $ }) {
const response = await this.app.modifyTimeEntry({
$,
accountId: this.accountId,
timeEntryId: this.timeEntryId,
data: {
startTime: this.startTime ?? "",
endTime: this.endTime ?? "",
note: this.note,
isBillable: this.isBillable,
isInvoiced: this.isInvoiced,
},
});
$.export("$summary", "Successfully modified the time entry with ID: " + this.timeEntryId);
return response;
},
};
5 changes: 4 additions & 1 deletion components/tmetric/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipedream/tmetric",
"version": "0.0.6",
"version": "0.1.0",
"description": "Pipedream TMetric Components",
"main": "tmetric.app.mjs",
"keywords": [
Expand All @@ -11,5 +11,8 @@
"author": "Pipedream <support@pipedream.com> (https://pipedream.com/)",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@pipedream/platform": "^3.1.1"
}
}
129 changes: 125 additions & 4 deletions components/tmetric/tmetric.app.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,132 @@
import { axios } from "@pipedream/platform";

export default {
type: "app",
app: "tmetric",
propDefinitions: {},
propDefinitions: {
accountId: {
type: "string",
label: "Account ID",
description: "Identifier of the account",
async options() {
const response = await this.getUser();
const accounts = response.accounts;
return accounts.map(({
id, name,
}) => ({
value: id,
label: name,
}));
},
},
timeEntryId: {
type: "string",
label: "Time Entry ID",
description: "Identifier of the time entry",
async options({ accountId }) {
const response = await this.getTimeEntries({
accountId,
});
return response.map(({
id, note,
}) => ({
value: id,
label: note,
}));
},
},
startTime: {
type: "string",
label: "Start Time",
description: "Start timestamp in ISO format, e.g. `2025-11-26T14:16:00`; leave null to start the timer at the current moment",
optional: true,
},
endTime: {
type: "string",
label: "End Time",
description: "End timestamp in ISO format, e.g. `2025-11-26T15:16:00`; leave null to keep the timer running indefinitely",
optional: true,
Comment on lines +39 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Clarify “leave null” phrasing in startTime / endTime descriptions

The descriptions say “leave null …”, but in the UI users will typically just leave these fields blank. Consider rephrasing to something like “leave blank to …” to better match how props are actually set:

-      description: "Start timestamp in ISO format, e.g. `2025-11-26T14:16:00`; leave null to start the timer at the current moment",
+      description: "Start timestamp in ISO format, e.g. `2025-11-26T14:16:00`; leave blank to start the timer at the current moment",
...
-      description: "End timestamp in ISO format, e.g. `2025-11-26T15:16:00`; leave null to keep the timer running indefinitely",
+      description: "End timestamp in ISO format, e.g. `2025-11-26T15:16:00`; leave blank to keep the timer running indefinitely",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type: "string",
label: "Start Time",
description: "Start timestamp in ISO format, e.g. `2025-11-26T14:16:00`; leave null to start the timer at the current moment",
optional: true,
},
endTime: {
type: "string",
label: "End Time",
description: "End timestamp in ISO format, e.g. `2025-11-26T15:16:00`; leave null to keep the timer running indefinitely",
optional: true,
type: "string",
label: "Start Time",
description: "Start timestamp in ISO format, e.g. `2025-11-26T14:16:00`; leave blank to start the timer at the current moment",
optional: true,
},
endTime: {
type: "string",
label: "End Time",
description: "End timestamp in ISO format, e.g. `2025-11-26T15:16:00`; leave blank to keep the timer running indefinitely",
optional: true,
🤖 Prompt for AI Agents
In components/tmetric/tmetric.app.mjs around lines 39 to 48, the field
descriptions for startTime and endTime use the phrase "leave null" which is
misleading for users who will typically leave the form fields blank; update both
descriptions to say "leave blank" (or "omit") and keep the examples and behavior
notes intact so the UI text accurately reflects how props are set when users
don't provide a value.

},
note: {
type: "string",
label: "Note",
description: "Additional note for the time entry",
optional: true,
},
isBillable: {
type: "boolean",
label: "Is Billable",
description: "Whether the time entry is billable",
optional: true,
},
isInvoiced: {
type: "boolean",
label: "Is Invoiced",
description: "Whether the time entry has been invoiced",
optional: true,
},
},
methods: {
// this.$auth contains connected account data
authKeys() {
console.log(Object.keys(this.$auth));
_baseUrl() {
return "https://app.tmetric.com/api/v3";
},
async _makeRequest(opts = {}) {
const {
$ = this,
path,
headers,
...otherOpts
} = opts;
return axios($, {
...otherOpts,
url: this._baseUrl() + path,
headers: {
Authorization: `Bearer ${this.$auth.api_token}`,
...headers,
},
});
},

async createTimeEntry({
accountId, ...args
}) {
return this._makeRequest({
path: `/accounts/${accountId}/timeentries`,
method: "post",
...args,
});
},
async deleteTimeEntry({
accountId, timeEntryId, ...args
}) {
return this._makeRequest({
path: `/accounts/${accountId}/timeentries/${timeEntryId}`,
method: "delete",
...args,
});
},
async modifyTimeEntry({
accountId, timeEntryId, ...args
}) {
return this._makeRequest({
path: `/accounts/${accountId}/timeentries/${timeEntryId}`,
method: "put",
...args,
});
},
async getTimeEntries({
accountId, ...args
}) {
return this._makeRequest({
path: `/accounts/${accountId}/timeentries`,
...args,
});
},
async getUser(args = {}) {
return this._makeRequest({
path: "/user",
...args,
});
},
},
};
12 changes: 7 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading