From d99e4a7bfb7b6a088b56aad4566a12f5b93fa431 Mon Sep 17 00:00:00 2001 From: Mark Horninger Date: Mon, 3 Feb 2020 17:11:35 -0500 Subject: [PATCH] Building out the skeleton for Clockify integration --- .../credentials/ClockifyApi.credentials.ts | 21 +++ .../nodes/Clockify/ClockifyTrigger.node.ts | 121 ++++++++++++++++++ .../nodes-base/nodes/Clockify/CommonDtos.ts | 19 +++ .../nodes/Clockify/EntryTypeEnum.ts | 3 + .../nodes/Clockify/GenericFunctions.ts | 38 ++++++ .../nodes-base/nodes/Clockify/UserDtos.ts | 20 +++ .../nodes/Clockify/WorkpaceInterfaces.ts | 77 +++++++++++ .../Clockify/images/clockify-mark-blue.png | Bin 0 -> 5955 bytes packages/nodes-base/package.json | 3 + 9 files changed, 302 insertions(+) create mode 100644 packages/nodes-base/credentials/ClockifyApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Clockify/CommonDtos.ts create mode 100644 packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts create mode 100644 packages/nodes-base/nodes/Clockify/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Clockify/UserDtos.ts create mode 100644 packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts create mode 100644 packages/nodes-base/nodes/Clockify/images/clockify-mark-blue.png diff --git a/packages/nodes-base/credentials/ClockifyApi.credentials.ts b/packages/nodes-base/credentials/ClockifyApi.credentials.ts new file mode 100644 index 0000000000..ce84ab2759 --- /dev/null +++ b/packages/nodes-base/credentials/ClockifyApi.credentials.ts @@ -0,0 +1,21 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class ClockifyApi implements ICredentialType { + name = 'clockifyApi'; + displayName = 'Clockify API'; + properties = [ + // The credentials to get from user and save encrypted. + // Properties can be defined exactly in the same way + // as node properties. + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts b/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts new file mode 100644 index 0000000000..a67e7037ba --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts @@ -0,0 +1,121 @@ +import {IPollFunctions} from 'n8n-core'; +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + clockifyApiRequest, +} from './GenericFunctions'; + +import {IWorkspaceDto} from "./WorkpaceInterfaces"; +import {EntryTypeEnum} from "./EntryTypeEnum"; +import {ICurrentUserDto} from "./UserDtos"; +import * as moment from "moment"; + + +export class ClockifyTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Clockify Event', + icon: 'file:images/clockify-mark-blue.png', + name: 'clockifyTrigger', + group: ['trigger'], + version: 1, + description: 'Watches Clockify For Events', + defaults: { + name: 'Clockify Event', + color: '#00FF00', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'clockifyApi', + required: true, + } + ], + properties: [ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'listWorkspaces', + }, + required: true, + default: '', + }, + { + displayName: 'Trigger', + name: 'watchField', + type: 'options', + options: [ + { + name: 'New Time Entry', + value: EntryTypeEnum.NEW_TIME_ENTRY, + } + ], + required: true, + default: '', + }, + ] + }; + + methods = { + loadOptions: { + async listWorkspaces(this: ILoadOptionsFunctions) : Promise { + const rtv : INodePropertyOptions[] = []; + const workspaces: IWorkspaceDto[] = await clockifyApiRequest.call(this,'GET', 'workspaces'); + if(undefined !== workspaces) { + workspaces.forEach(value => { + rtv.push( + { + name: value.name, + value: value.id, + }); + }); + } + return rtv; + }, + }, + }; + + async poll(this: IPollFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const triggerField = this.getNodeParameter('watchField') as EntryTypeEnum; + const workspaceId = this.getNodeParameter('workspaceId'); + + const userInfo : ICurrentUserDto = await clockifyApiRequest.call(this,'GET', 'user'); + const qs : IDataObject = {}; + let resource: string; + let result = null; + + switch (triggerField) { + case EntryTypeEnum.NEW_TIME_ENTRY : + default: + resource = `workspaces/${workspaceId}/user/${userInfo.id}/time-entries`; + qs.start = webhookData.lastTimeChecked; + qs.end = moment().toISOString(); + qs.hydrated = true; + qs['in-progress'] = false; + break; + } + console.error(qs); + try { + result = await clockifyApiRequest.call(this, 'GET', resource, {}, qs ); + webhookData.lastTimeChecked = qs.end_date; + } + catch( e ) { + throw new Error(`Clockify Exception: ${e}`); + } + if (Array.isArray(result) && result.length !== 0) { + result = [this.helpers.returnJsonArray(result)]; + } + return result; + + } +} diff --git a/packages/nodes-base/nodes/Clockify/CommonDtos.ts b/packages/nodes-base/nodes/Clockify/CommonDtos.ts new file mode 100644 index 0000000000..b5449c5c5c --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/CommonDtos.ts @@ -0,0 +1,19 @@ +export interface IHourlyRateDto { + amount: number; + currency: string; +} + +enum MembershipStatusEnum { + PENDING = "PENDING", + ACTIVE = "ACTIVE", + DECLINED = "DECLINED", + INACTIVE = "INACTIVE" +} + +export interface IMembershipDto { + hourlyRate: IHourlyRateDto; + membershipStatus: MembershipStatusEnum; + membershipType: string; + targetId: string; + userId: string; +} diff --git a/packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts b/packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts new file mode 100644 index 0000000000..0df2a71019 --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts @@ -0,0 +1,3 @@ +export enum EntryTypeEnum { + NEW_TIME_ENTRY, +} diff --git a/packages/nodes-base/nodes/Clockify/GenericFunctions.ts b/packages/nodes-base/nodes/Clockify/GenericFunctions.ts new file mode 100644 index 0000000000..62a5943500 --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/GenericFunctions.ts @@ -0,0 +1,38 @@ +import { OptionsWithUri } from 'request'; +import { + ILoadOptionsFunctions, + IPollFunctions +} from "n8n-core"; + +import {IDataObject} from "n8n-workflow"; + +export async function clockifyApiRequest(this: ILoadOptionsFunctions | IPollFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('clockifyApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const BASE_URL = `https://api.clockify.me/api/v1`; + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + 'X-Api-Key': credentials.apiKey as string, + }, + method, + qs, + body, + uri: `${BASE_URL}/${resource}`, + json: true + }; + try { + return await this.helpers.request!(options); + } catch (error) { + + let errorMessage = error.message; + if (error.response.body && error.response.body.message) { + errorMessage = `[${error.response.body.status_code}] ${error.response.body.message}`; + } + + throw new Error('Clockify Error: ' + errorMessage); + } +} diff --git a/packages/nodes-base/nodes/Clockify/UserDtos.ts b/packages/nodes-base/nodes/Clockify/UserDtos.ts new file mode 100644 index 0000000000..cf7ee15813 --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/UserDtos.ts @@ -0,0 +1,20 @@ +import {IMembershipDto} from "./CommonDtos"; + +enum UserStatusEnum { + ACTIVE, PENDING_EMAIL_VERIFICATION, DELETED +} + +interface IUserSettingsDto { +} + +export interface ICurrentUserDto { + activeWorkspace: string; + defaultWorkspace: string; + email: string; + id: string; + memberships: IMembershipDto []; + name: string; + profilePicture: string; + settings: IUserSettingsDto; + status: UserStatusEnum; +} diff --git a/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts b/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts new file mode 100644 index 0000000000..fdf0c005ec --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts @@ -0,0 +1,77 @@ +import {IHourlyRateDto, IMembershipDto} from "./CommonDtos"; + +enum AdminOnlyPagesEnum { + PROJECT ="PROJECT", + TEAM = "TEAM", + REPORTS = "REPORTS" +} + +enum DaysOfWeekEnum { + MONDAY = "MONDAY", + TUESDAY = "TUESDAY", + WEDNESDAY = "WEDNESDAY", + THURSDAY = "THURSDAY", + FRIDAY = "FRIDAY", + SATURDAY = "SATURDAY", + SUNDAY = "SUNDAY" +} + +enum DatePeriodEnum { + DAYS="DAYS", + WEEKS = "WEEKS", + MONTHS = "MONTHS" +} + +enum AutomaticLockTypeEnum { + WEEKLY = "WEEKLY", + MONTHLY = "MONTHLY", + OLDER_THAN = "OLDER_THAN" +} + +interface IAutomaticLockDto { + changeDay: DaysOfWeekEnum; + dayOfMonth: number; + firstDay: DaysOfWeekEnum; + olderThanPeriod: DatePeriodEnum; + olderThanValue: number; + type: AutomaticLockTypeEnum; +} + +interface IRound { + minutes: string; + round: string; +} + +interface IWorkspaceSettingsDto { + adminOnlyPages: AdminOnlyPagesEnum[]; + automaticLock: IAutomaticLockDto; + canSeeTimeSheet: boolean; + defaultBillableProjects: boolean; + forceDescription: boolean; + forceProjects: boolean; + forceTags: boolean; + forceTasks: boolean; + lockTimeEntries: string; + onlyAdminsCreateProject: boolean; + onlyAdminsCreateTag: boolean; + onlyAdminsSeeAllTimeEntries: boolean; + onlyAdminsSeeBillableRates: boolean; + onlyAdminsSeeDashboard: boolean; + onlyAdminsSeePublicProjectsEntries: boolean; + projectFavorites: boolean; + projectGroupingLabel: string; + projectPickerSpecialFilter: boolean; + round: IRound; + timeRoundingInReports: boolean; + trackTimeDownToSecond: boolean; +} + +export interface IWorkspaceDto { + hourlyRate: IHourlyRateDto; + id: string; + imageUrl: string; + memberships: IMembershipDto[]; + name: string; + workspaceSettings: IWorkspaceSettingsDto; +} + diff --git a/packages/nodes-base/nodes/Clockify/images/clockify-mark-blue.png b/packages/nodes-base/nodes/Clockify/images/clockify-mark-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..ac4c44c7632112188285f1602a7d383dadb47c0b GIT binary patch literal 5955 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 SL?(OY4b08XnpPOQ$NnG5MH@>1 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b59b37befd..6c9be610b7 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -37,6 +37,7 @@ "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/ClearbitApi.credentials.js", "dist/credentials/ClickUpApi.credentials.js", + "dist/credentials/ClockifyApi.credentials.js", "dist/credentials/CodaApi.credentials.js", "dist/credentials/CopperApi.credentials.js", "dist/credentials/DisqusApi.credentials.js", @@ -112,6 +113,7 @@ "dist/nodes/Clearbit/Clearbit.node.js", "dist/nodes/ClickUp/ClickUp.node.js", "dist/nodes/ClickUp/ClickUpTrigger.node.js", + "dist/nodes/Clockify/ClockifyTrigger.node.js", "dist/nodes/Coda/Coda.node.js", "dist/nodes/Copper/CopperTrigger.node.js", "dist/nodes/Cron.node.js", @@ -283,3 +285,4 @@ ] } } +