@pipedream/harvest 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/actions/create-timesheet-entry/create-timesheet-entry.mjs +93 -0
- package/actions/start-timer/start-timer.mjs +29 -0
- package/actions/stop-timer/stop-timer.mjs +29 -0
- package/common/constants.mjs +12 -0
- package/common/utils.mjs +66 -0
- package/harvest.app.mjs +307 -0
- package/package.json +21 -0
- package/sources/new-invoice-entry/new-invoice-entry.mjs +45 -0
- package/sources/new-timesheet-entry/new-timesheet-entry.mjs +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2020 Pipedream, Inc.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ConfigurationError } from "@pipedream/platform";
|
|
2
|
+
import harvest from "../../harvest.app.mjs";
|
|
3
|
+
import {
|
|
4
|
+
isValidDate, isValidTime, removeNullEntries,
|
|
5
|
+
} from "../../common/utils.mjs";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
key: "harvest-create-timesheet-entry",
|
|
9
|
+
name: "Create Timesheet Entry",
|
|
10
|
+
description: `Creates a new time entry object.
|
|
11
|
+
[Create a time entry via duration documentation](https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#create-a-time-entry-via-duration),
|
|
12
|
+
[Create a time entry via start and end time documentation](https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#create-a-time-entry-via-start-and-end-time)`,
|
|
13
|
+
version: "0.0.1",
|
|
14
|
+
type: "action",
|
|
15
|
+
props: {
|
|
16
|
+
harvest,
|
|
17
|
+
projectId: {
|
|
18
|
+
propDefinition: [
|
|
19
|
+
harvest,
|
|
20
|
+
"projectId",
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
taskId: {
|
|
24
|
+
propDefinition: [
|
|
25
|
+
harvest,
|
|
26
|
+
"taskId",
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
spentDate: {
|
|
30
|
+
type: "string",
|
|
31
|
+
label: "Spent date (YYYY-MM-DD)",
|
|
32
|
+
description: "The ISO 8601 formatted date on which the time entry was spent. Example: 2019-07-26",
|
|
33
|
+
},
|
|
34
|
+
userId: {
|
|
35
|
+
propDefinition: [
|
|
36
|
+
harvest,
|
|
37
|
+
"userId",
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
specifyStartEndTime: {
|
|
41
|
+
type: "boolean",
|
|
42
|
+
label: "Specify start and end time",
|
|
43
|
+
description: "Specify start and end time",
|
|
44
|
+
reloadProps: true,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
async additionalProps() {
|
|
48
|
+
const props = {};
|
|
49
|
+
if (this.specifyStartEndTime === true) {
|
|
50
|
+
props.startedTime = {
|
|
51
|
+
type: "string",
|
|
52
|
+
label: "Start time(H:MM am/pm)",
|
|
53
|
+
description: "The time the entry started. Defaults to the current time. Example: 8:00am.",
|
|
54
|
+
};
|
|
55
|
+
props.endedTime = {
|
|
56
|
+
type: "string",
|
|
57
|
+
label: "End time(H:MM am/pm)",
|
|
58
|
+
description: "The time the entry ended. Defaults to the current time. Example: 8:00am.",
|
|
59
|
+
optional: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return props;
|
|
63
|
+
},
|
|
64
|
+
async run({ $ }) {
|
|
65
|
+
|
|
66
|
+
if (this.spentDate && !isValidDate(this.spentDate)) {
|
|
67
|
+
throw new ConfigurationError("Invalid spent date. Ensure format is YYYY-MM-DD");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (this.startedTime && !isValidTime(this.startedTime)) {
|
|
71
|
+
throw new ConfigurationError("Invalid start time. Ensure format is (H:MM am/pm)");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (this.endedTime && !isValidTime(this.endedTime)) {
|
|
75
|
+
throw new ConfigurationError("Invalid end time. Ensure format is (H:MM am/pm)");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const params = removeNullEntries({
|
|
79
|
+
project_id: this.projectId,
|
|
80
|
+
task_id: this.taskId,
|
|
81
|
+
user_id: this.userId,
|
|
82
|
+
spent_date: this.spentDate,
|
|
83
|
+
started_time: this.startedTime.replace(/\s/g, ""),
|
|
84
|
+
ended_time: this.endedTime.replace(/\s/g, ""),
|
|
85
|
+
});
|
|
86
|
+
const response = await this.harvest.createTimeEntry({
|
|
87
|
+
$,
|
|
88
|
+
params,
|
|
89
|
+
});
|
|
90
|
+
response && $.export("$summary", "Successfully created time entry");
|
|
91
|
+
return response;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import harvest from "../../harvest.app.mjs";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
key: "harvest-start-timer",
|
|
5
|
+
name: "Start Time Entry",
|
|
6
|
+
description: "Restart a stopped timer entry. [See docs here](https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#restart-a-stopped-time-entry)",
|
|
7
|
+
version: "0.0.1",
|
|
8
|
+
type: "action",
|
|
9
|
+
props: {
|
|
10
|
+
harvest,
|
|
11
|
+
timeEntryId: {
|
|
12
|
+
propDefinition: [
|
|
13
|
+
harvest,
|
|
14
|
+
"timeEntryId",
|
|
15
|
+
() => ({
|
|
16
|
+
isRunning: false,
|
|
17
|
+
}),
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
async run({ $ }) {
|
|
22
|
+
const response = await this.harvest.restartTimeEntry({
|
|
23
|
+
$,
|
|
24
|
+
id: this.timeEntryId,
|
|
25
|
+
});
|
|
26
|
+
response && $.export("$summary", "Successfully started the time entry");
|
|
27
|
+
return response;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import harvest from "../../harvest.app.mjs";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
key: "harvest-start-timer",
|
|
5
|
+
name: "Stop Time Entry",
|
|
6
|
+
description: "Stop a timer entry. [See docs here](https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#stop-a-running-time-entry)",
|
|
7
|
+
version: "0.0.1",
|
|
8
|
+
type: "action",
|
|
9
|
+
props: {
|
|
10
|
+
harvest,
|
|
11
|
+
timeEntryId: {
|
|
12
|
+
propDefinition: [
|
|
13
|
+
harvest,
|
|
14
|
+
"timeEntryId",
|
|
15
|
+
() => ({
|
|
16
|
+
isRunning: true,
|
|
17
|
+
}),
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
async run({ $ }) {
|
|
22
|
+
const response = await this.harvest.stopTimeEntry({
|
|
23
|
+
$,
|
|
24
|
+
id: this.timeEntryId,
|
|
25
|
+
});
|
|
26
|
+
response && $.export("$summary", "Successfully ended the time entry");
|
|
27
|
+
return response;
|
|
28
|
+
},
|
|
29
|
+
};
|
package/common/utils.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const removeNullEntries = (obj) =>
|
|
2
|
+
obj && Object.entries(obj).reduce((acc, [
|
|
3
|
+
key,
|
|
4
|
+
value,
|
|
5
|
+
]) => {
|
|
6
|
+
const isNumber = typeof value === "number";
|
|
7
|
+
const isBoolean = typeof value === "boolean";
|
|
8
|
+
const isNotEmpyString = typeof value === "string" && value.trim() !== "";
|
|
9
|
+
const isNotEmptyArray = Array.isArray(value) && value.length;
|
|
10
|
+
const isNotEmptyObject =
|
|
11
|
+
typeof value === "object" &&
|
|
12
|
+
value !== null &&
|
|
13
|
+
!Array.isArray(value) &&
|
|
14
|
+
Object.keys(value).length !== 0;
|
|
15
|
+
isNotEmptyObject && (value = removeNullEntries(value));
|
|
16
|
+
return ((value || value === false) &&
|
|
17
|
+
(isNotEmpyString || isNotEmptyArray || isNotEmptyObject || isBoolean || isNumber))
|
|
18
|
+
? {
|
|
19
|
+
...acc,
|
|
20
|
+
[key]: value,
|
|
21
|
+
}
|
|
22
|
+
: acc;
|
|
23
|
+
}, {});
|
|
24
|
+
|
|
25
|
+
/* This function checks for date strings with the format YYYY-MM-DD
|
|
26
|
+
examples
|
|
27
|
+
2022-09-28
|
|
28
|
+
2021-12-10
|
|
29
|
+
*/
|
|
30
|
+
const isValidDate = (dateString) => {
|
|
31
|
+
const regex = /^\d{4}-\d{2}-\d{2}$/;
|
|
32
|
+
const date = new Date(dateString);
|
|
33
|
+
if (!dateString) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (dateString.match(regex) === null) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const timestamp = date.getTime();
|
|
40
|
+
if (typeof timestamp !== "number" || Number.isNaN(timestamp)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return date.toISOString().startsWith(dateString);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/* This function checks for time strings with the format HH:MM AM/PM
|
|
47
|
+
examples
|
|
48
|
+
11:45PM
|
|
49
|
+
11:45 PM
|
|
50
|
+
11:45 pm
|
|
51
|
+
11:45pm
|
|
52
|
+
11:45AM
|
|
53
|
+
11:45 AM
|
|
54
|
+
11:45 am
|
|
55
|
+
11:45am
|
|
56
|
+
*/
|
|
57
|
+
const isValidTime = (timeString) => {
|
|
58
|
+
const regex = /([0-9]|0[0-9]|1[0-9]|2[0-3]):([0-5][0-9])\s*([AaPp][Mm])$/;
|
|
59
|
+
return timeString
|
|
60
|
+
? timeString.match(regex)
|
|
61
|
+
: false;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
removeNullEntries, isValidDate, isValidTime,
|
|
66
|
+
};
|
package/harvest.app.mjs
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import constants from "./common/constants.mjs";
|
|
2
|
+
import {
|
|
3
|
+
axios, ConfigurationError,
|
|
4
|
+
} from "@pipedream/platform";
|
|
5
|
+
import retry from "async-retry";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
type: "app",
|
|
9
|
+
app: "harvest",
|
|
10
|
+
propDefinitions: {
|
|
11
|
+
projectId: {
|
|
12
|
+
type: "string",
|
|
13
|
+
label: "Project ID",
|
|
14
|
+
description: "The ID of the project to associate with the time entry",
|
|
15
|
+
useQuery: true,
|
|
16
|
+
async options({ page }) {
|
|
17
|
+
const response = await this.listProjects({
|
|
18
|
+
perPage: constants.PAGE_SIZE,
|
|
19
|
+
page: page + 1,
|
|
20
|
+
});
|
|
21
|
+
return response.projects.map((project) => ({
|
|
22
|
+
label: project.name,
|
|
23
|
+
value: project.id,
|
|
24
|
+
}));
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
taskId: {
|
|
28
|
+
type: "string",
|
|
29
|
+
label: "Task ID",
|
|
30
|
+
description: "The ID of the task to associate with the time entry",
|
|
31
|
+
useQuery: true,
|
|
32
|
+
async options({ page }) {
|
|
33
|
+
const response = await this.listTasks({
|
|
34
|
+
perPage: constants.PAGE_SIZE,
|
|
35
|
+
page: page + 1,
|
|
36
|
+
});
|
|
37
|
+
return response.tasks.map((task) => ({
|
|
38
|
+
label: task.name,
|
|
39
|
+
value: task.id,
|
|
40
|
+
}));
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
userId: {
|
|
44
|
+
type: "string",
|
|
45
|
+
label: "User ID",
|
|
46
|
+
description: "The ID of the user to associate with the time entry",
|
|
47
|
+
useQuery: true,
|
|
48
|
+
optional: true,
|
|
49
|
+
async options({ page }) {
|
|
50
|
+
const response = await this.listUsers({
|
|
51
|
+
perPage: constants.PAGE_SIZE,
|
|
52
|
+
page: page + 1,
|
|
53
|
+
});
|
|
54
|
+
return response.users.map((user) => ({
|
|
55
|
+
label: `${user.first_name} ${user.last_name}`,
|
|
56
|
+
value: user.id,
|
|
57
|
+
}));
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
clientId: {
|
|
61
|
+
type: "string",
|
|
62
|
+
label: "Client ID",
|
|
63
|
+
description: "The ID of the client to associate with time entries",
|
|
64
|
+
useQuery: true,
|
|
65
|
+
optional: true,
|
|
66
|
+
async options({ page }) {
|
|
67
|
+
const response = await this.listClients({
|
|
68
|
+
perPage: constants.PAGE_SIZE,
|
|
69
|
+
page: page + 1,
|
|
70
|
+
});
|
|
71
|
+
return response.clients.map((client) => ({
|
|
72
|
+
label: client.name,
|
|
73
|
+
value: client.id,
|
|
74
|
+
}));
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
timeEntryId: {
|
|
78
|
+
type: "string",
|
|
79
|
+
label: "Time entry ID",
|
|
80
|
+
description: "The ID of the time entry.",
|
|
81
|
+
useQuery: true,
|
|
82
|
+
async options({
|
|
83
|
+
page, isRunning,
|
|
84
|
+
}) {
|
|
85
|
+
const response = await this.listTimeEntries({
|
|
86
|
+
perPage: constants.PAGE_SIZE,
|
|
87
|
+
page: page + 1,
|
|
88
|
+
is_running: isRunning,
|
|
89
|
+
});
|
|
90
|
+
return response.time_entries.map((entry) => ({
|
|
91
|
+
label: `Project: ${entry.project.name}, Task: ${entry.task.name}, Spend date: ${entry.spent_date} ${entry.started_time || ""} ${entry.ended_time
|
|
92
|
+
? " to " + entry.ended_time
|
|
93
|
+
: ""}`,
|
|
94
|
+
value: entry.id,
|
|
95
|
+
}));
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
methods: {
|
|
100
|
+
setLastDateChecked(db, value) {
|
|
101
|
+
db && db.set(constants.DB_LAST_DATE_CHECK, value);
|
|
102
|
+
},
|
|
103
|
+
getLastDateChecked(db) {
|
|
104
|
+
return db && db.get(constants.DB_LAST_DATE_CHECK);
|
|
105
|
+
},
|
|
106
|
+
_getHeaders() {
|
|
107
|
+
return {
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
"Authorization": `Bearer ${this.$auth.oauth_access_token}`,
|
|
110
|
+
"Harvest-Account-Id": this.$auth.account_id,
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
_getUrl(path) {
|
|
114
|
+
const {
|
|
115
|
+
BASE_URL,
|
|
116
|
+
HTTP_PROTOCOL,
|
|
117
|
+
VERSION_PATH,
|
|
118
|
+
} = constants;
|
|
119
|
+
return `${HTTP_PROTOCOL}${BASE_URL}${VERSION_PATH}${path}`;
|
|
120
|
+
},
|
|
121
|
+
async _makeRequest(args = {}) {
|
|
122
|
+
const {
|
|
123
|
+
$,
|
|
124
|
+
method = "get",
|
|
125
|
+
path,
|
|
126
|
+
params,
|
|
127
|
+
data,
|
|
128
|
+
} = args;
|
|
129
|
+
const config = {
|
|
130
|
+
method,
|
|
131
|
+
url: this._getUrl(path),
|
|
132
|
+
headers: this._getHeaders(),
|
|
133
|
+
params,
|
|
134
|
+
data,
|
|
135
|
+
};
|
|
136
|
+
return axios($ ?? this, config);
|
|
137
|
+
},
|
|
138
|
+
_isRetriableStatusCode(statusCode) {
|
|
139
|
+
constants.retriableStatusCodes.includes(statusCode);
|
|
140
|
+
},
|
|
141
|
+
async _withRetries(apiCall) {
|
|
142
|
+
const retryOpts = {
|
|
143
|
+
retries: 5,
|
|
144
|
+
factor: 2,
|
|
145
|
+
};
|
|
146
|
+
return retry(async (bail) => {
|
|
147
|
+
try {
|
|
148
|
+
const data = await apiCall();
|
|
149
|
+
|
|
150
|
+
return data;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
const { status = 500 } = err;
|
|
153
|
+
if (!this._isRetriableStatusCode(status)) {
|
|
154
|
+
bail(`
|
|
155
|
+
Unexpected error (status code: ${status}):
|
|
156
|
+
${JSON.stringify(err.response)}
|
|
157
|
+
`);
|
|
158
|
+
}
|
|
159
|
+
throw new ConfigurationError("Could not get data");
|
|
160
|
+
}
|
|
161
|
+
}, retryOpts);
|
|
162
|
+
},
|
|
163
|
+
async *listTimeEntriesPaginated({
|
|
164
|
+
page, updatedSince,
|
|
165
|
+
}) {
|
|
166
|
+
do {
|
|
167
|
+
const response = await this._withRetries(
|
|
168
|
+
() => this.listTimeEntries({
|
|
169
|
+
per_page: constants.PAGE_SIZE,
|
|
170
|
+
page,
|
|
171
|
+
updated_since: updatedSince,
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (response.time_entries.length === 0) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
for (const entry of response.time_entries) {
|
|
179
|
+
yield entry;
|
|
180
|
+
}
|
|
181
|
+
if (!response.next_page) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
page += 1;
|
|
185
|
+
} while (true);
|
|
186
|
+
},
|
|
187
|
+
async *listInvoicesPaginated({
|
|
188
|
+
page, updatedSince,
|
|
189
|
+
}) {
|
|
190
|
+
do {
|
|
191
|
+
const response = await this._withRetries(
|
|
192
|
+
() => this.listInvoices({
|
|
193
|
+
per_page: constants.PAGE_SIZE,
|
|
194
|
+
page,
|
|
195
|
+
updated_since: updatedSince,
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (response.invoices.length === 0) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
for (const invoice of response.invoices) {
|
|
203
|
+
yield invoice;
|
|
204
|
+
}
|
|
205
|
+
if (!response.next_page) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
page += 1;
|
|
209
|
+
} while (true);
|
|
210
|
+
},
|
|
211
|
+
async listProjects({
|
|
212
|
+
$, perPage, page,
|
|
213
|
+
}) {
|
|
214
|
+
return this._makeRequest({
|
|
215
|
+
$,
|
|
216
|
+
path: "/projects",
|
|
217
|
+
params: {
|
|
218
|
+
per_page: perPage,
|
|
219
|
+
page,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
async listTasks({
|
|
224
|
+
$, perPage, page,
|
|
225
|
+
}) {
|
|
226
|
+
return this._makeRequest({
|
|
227
|
+
$,
|
|
228
|
+
path: "/tasks",
|
|
229
|
+
params: {
|
|
230
|
+
per_page: perPage,
|
|
231
|
+
page,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
async listUsers({
|
|
236
|
+
$, perPage, page,
|
|
237
|
+
}) {
|
|
238
|
+
return this._makeRequest({
|
|
239
|
+
$,
|
|
240
|
+
path: "/users",
|
|
241
|
+
params: {
|
|
242
|
+
per_page: perPage,
|
|
243
|
+
page,
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
async listClients({
|
|
248
|
+
$, perPage, page,
|
|
249
|
+
}) {
|
|
250
|
+
return this._makeRequest({
|
|
251
|
+
$,
|
|
252
|
+
path: "/clients",
|
|
253
|
+
params: {
|
|
254
|
+
per_page: perPage,
|
|
255
|
+
page,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
async createTimeEntry({
|
|
260
|
+
$, params,
|
|
261
|
+
}) {
|
|
262
|
+
return this._makeRequest({
|
|
263
|
+
$,
|
|
264
|
+
path: "/time_entries",
|
|
265
|
+
params,
|
|
266
|
+
method: "post",
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
async listTimeEntries({
|
|
270
|
+
$, ...params
|
|
271
|
+
}) {
|
|
272
|
+
|
|
273
|
+
return this._makeRequest({
|
|
274
|
+
$,
|
|
275
|
+
path: "/time_entries",
|
|
276
|
+
params,
|
|
277
|
+
});
|
|
278
|
+
},
|
|
279
|
+
async restartTimeEntry({
|
|
280
|
+
$, id,
|
|
281
|
+
}) {
|
|
282
|
+
return this._makeRequest({
|
|
283
|
+
$,
|
|
284
|
+
path: `/time_entries/${id}/restart`,
|
|
285
|
+
method: "patch",
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
async stopTimeEntry({
|
|
289
|
+
$, id,
|
|
290
|
+
}) {
|
|
291
|
+
return this._makeRequest({
|
|
292
|
+
$,
|
|
293
|
+
path: `/time_entries/${id}/stop`,
|
|
294
|
+
method: "patch",
|
|
295
|
+
});
|
|
296
|
+
},
|
|
297
|
+
async listInvoices({
|
|
298
|
+
$, ...params
|
|
299
|
+
}) {
|
|
300
|
+
return this._makeRequest({
|
|
301
|
+
$,
|
|
302
|
+
path: "/invoices",
|
|
303
|
+
params,
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pipedream/harvest",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Pipedream Harvest Components",
|
|
5
|
+
"main": "harvest.app.mjs",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pipedream",
|
|
8
|
+
"harvest"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://pipedream.com/apps/harvest",
|
|
11
|
+
"author": "Pipedream <support@pipedream.com> (https://pipedream.com/)",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@pipedream/platform": "^0.9.0",
|
|
18
|
+
"moment": "^2.29.3",
|
|
19
|
+
"async-retry": "^1.3.3"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import harvest from "../../harvest.app.mjs";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
key: "harvest-new-invoice-entry",
|
|
5
|
+
name: "New Invoice Entry",
|
|
6
|
+
description: "Emit new notifications when a new invoice is created",
|
|
7
|
+
version: "0.0.1",
|
|
8
|
+
type: "source",
|
|
9
|
+
props: {
|
|
10
|
+
harvest,
|
|
11
|
+
timer: {
|
|
12
|
+
label: "Polling interval",
|
|
13
|
+
description: "Pipedream will poll Harvest API on this schedule",
|
|
14
|
+
type: "$.interface.timer",
|
|
15
|
+
default: {
|
|
16
|
+
intervalSeconds: 60 * 15,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
db: "$.service.db",
|
|
20
|
+
},
|
|
21
|
+
dedupe: "unique",
|
|
22
|
+
async run() {
|
|
23
|
+
const data = [];
|
|
24
|
+
let lastDateChecked = this.harvest.getLastDateChecked(this.db);
|
|
25
|
+
if (!lastDateChecked) {
|
|
26
|
+
lastDateChecked = new Date().toISOString();
|
|
27
|
+
this.harvest.setLastDateChecked(this.db, lastDateChecked);
|
|
28
|
+
}
|
|
29
|
+
const invoices = await this.harvest.listInvoicesPaginated({
|
|
30
|
+
page: 1,
|
|
31
|
+
updatedSince: lastDateChecked,
|
|
32
|
+
});
|
|
33
|
+
for await (const invoice of invoices) {
|
|
34
|
+
data.push(invoice);
|
|
35
|
+
}
|
|
36
|
+
data && data.reverse().forEach((invoice) => {
|
|
37
|
+
this.harvest.setLastDateChecked(this.db, invoice.updated_at);
|
|
38
|
+
this.$emit(invoice,
|
|
39
|
+
{
|
|
40
|
+
id: invoice.id,
|
|
41
|
+
summary: `Invoice number: ${invoice.number}`,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import harvest from "../../harvest.app.mjs";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
key: "harvest-new-timesheet-entry",
|
|
5
|
+
name: "New Timesheet Entry",
|
|
6
|
+
description: "Emit new notifications when a new timesheet is created",
|
|
7
|
+
version: "0.0.1",
|
|
8
|
+
type: "source",
|
|
9
|
+
props: {
|
|
10
|
+
harvest,
|
|
11
|
+
timer: {
|
|
12
|
+
label: "Polling interval",
|
|
13
|
+
description: "Pipedream will poll Harvest API on this schedule",
|
|
14
|
+
type: "$.interface.timer",
|
|
15
|
+
default: {
|
|
16
|
+
intervalSeconds: 60 * 15,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
db: "$.service.db",
|
|
20
|
+
},
|
|
21
|
+
dedupe: "unique",
|
|
22
|
+
async run() {
|
|
23
|
+
const data = [];
|
|
24
|
+
let lastDateChecked = this.harvest.getLastDateChecked(this.db);
|
|
25
|
+
if (!lastDateChecked) {
|
|
26
|
+
lastDateChecked = new Date().toISOString();
|
|
27
|
+
this.harvest.setLastDateChecked(this.db, lastDateChecked);
|
|
28
|
+
}
|
|
29
|
+
const entries = await this.harvest.listTimeEntriesPaginated({
|
|
30
|
+
page: 1,
|
|
31
|
+
updatedSince: lastDateChecked,
|
|
32
|
+
});
|
|
33
|
+
for await (const entry of entries) {
|
|
34
|
+
data.push(entry);
|
|
35
|
+
}
|
|
36
|
+
data && data.reverse().forEach((entry) => {
|
|
37
|
+
this.harvest.setLastDateChecked(this.db, entry.updated_at);
|
|
38
|
+
this.$emit(entry,
|
|
39
|
+
{
|
|
40
|
+
id: entry.id,
|
|
41
|
+
summary: `Task: ${entry.task.name} - ${entry.spent_date} ${entry.started_time || ""} ${entry.ended_time
|
|
42
|
+
? " to " + entry.ended_time
|
|
43
|
+
: ""}`,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|