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..7b14182ffe --- /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; + } + + 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 0000000000..ac4c44c763 Binary files /dev/null and b/packages/nodes-base/nodes/Clockify/images/clockify-mark-blue.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 150c7e1332..f6fc8bae50 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", @@ -118,6 +119,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", @@ -293,3 +295,4 @@ ] } } +