Code Monkey home page Code Monkey logo

clockodo's Introduction

Clockodo

Unofficial JavaScript/TypeScript SDK for Clockodo.

Version on NPM Semantically released Monthly downloads on NPM
NPM Bundle size minified NPM Bundle size minified and gzipped
License

Installation and usage

npm install clockodo

For the constructor arguments, you must get the user (email) and clockodo API key from the "My area" section of Clockodo's website.

import { Clockodo } from "clockodo";

const clockodo = new Clockodo({
  client: {
    // You need to add some information about yourself that will be
    // sent along every request,
    // see https://www.clockodo.com/en/api/ "Client identification"
    // PLEASE NOTE: name + ";" + email must not be longer than 50 characters.
    name: "Your application/company",
    email: "[email protected]",
  },
  authentication: {
    user: "[email protected]",
    // You can get your API key from https://my.clockodo.com/en/users/editself
    apiKey: "kjfdskj643fgnlksf343kdslm",
  },
});

Config

  • client: Specify a name and an email for the X-Clockodo-External-Application header
  • authentication: Specify a user and an apiKey to authenticate every request
  • baseUrl: Points to the Clockodo API. Defaults to https://my.clockodo.com/api

You can update the configuration later like this:

clockodo.api.config({
  authentication: {
    /* ... */
  },
});

API

We have provided methods for each of the endpoints available by the Clockodo API. In order to provide a seamless API to JavaScript, we renamed the request and response object keys from what you will see in the Clockodo docs by removing special characters and converting to camel casing. If you are interested, you can find the mappings in the mappings.ts file.

For any questions about the different properties please consult the official Clockodo-API.

Some constants are also available for import:

import { EntryType, Billability, AbsenceStatus, AbsenceType } from "clockodo";

console.log(EntryType.Time); // 1
console.log(EntryType.LumpsumValue); // 2
console.log(EntryType.LumpsumService); // 3

console.log(Billability.NotBillable); // 0
console.log(Billability.Billable); // 1
console.log(Billability.Billed); // 2

Checkout models for more constants and TypeScript types.


Get Methods

getAbsence()

Gets a selected absence by its ID.

Example:

await clockodo.getAbsence({ id: 7 });

getUsersAccessCustomersProjects()

Gets a user's (readonly) access rights for customers and projects.

Example:

await clockodo.getUsersAccessCustomersProjects({ usersId: 67325 });

getUsersAccessServices()

Gets a user's (readonly) access rights for services.

Example:

await clockodo.getUsersAccessServices({ usersId: 67325 });

getAbsences()

Gets a list of absences in the provided year

Example:

await clockodo.getAbsences({ year: 2018 });

getClock()

Get currently running entry for the credentials attached to Clockodo object.

Example:

await clockodo.getClock();

getCustomer()

Get specific customer by ID

Example:

await clockodo.getCustomer({ id: 777 });

getCustomers()

Get all customers from all pages.

Example:

await clockodo.getCustomers();
// or
await clockodo.getCustomers({
  // Filter by active flag
  filterActive: true,
});

getCustomersPage()

Get all customers from a specific page.

Example:

await clockodo.getCustomersPage({ page: 2 });

getEntry()

Get an entry by its ID.

Example:

await clockodo.getEntry({ id: 4 });

getEntries()

Get all entries from all pages.

Example:

import { Billability } from "clockodo";

await clockodo.getEntries({
  // timeSince and timeUntil are required
  timeSince: "2017-08-18T00:00:00Z",
  timeUntil: "2018-02-09T00:00:00Z",
  // You can also add additional filters here
  filterBillable: Billability.Billed,
});

getEntriesPage()

Get all entries from a specific page

Example:

await clockodo.getEntriesPage({
  timeSince: "2017-08-18T00:00:00Z",
  timeUntil: "2018-02-09T00:00:00Z",
  page: 2,
});

getEntryGroups()

Get a group of entries defined by your criteria.

Example:

await clockodo.getEntryGroups({
  timeSince: "2017-08-18T00:00:00Z",
  timeUntil: "2018-02-09T00:00:00Z",
  grouping: ["customersId", "projectsId"],
  roundToMinutes: 15,
});

getEntriesTexts()

Retreive all descriptions (and no additional info) entered for time and lump sum entries from all pages.

Example:

await clockodo.getEntriesTexts({ text: "meeting with client" });

getEntriesTextsPage()

Retreive all descriptions from a specific page.

Example:

await clockodo.getEntriesTextsPage({ text: "meeting with client", page: 2 });

getProject()

Get a project by its ID.

Example:

await clockodo.getProject({ id: 1985 });

getProjects()

Get all projects from all pages.

Example:

await clockodo.getProjects();
// or
await clockodo.getProjects({
  // Filter by a specific customer id
  filterCustomersId: 123,
  // Filter by active flag
  filterActive: true,
});

getProjectsPage()

Get all projects from a specific page.

Example:

await clockodo.getProjectsPage({ page: 2 });

getService()

Get a service by its ID.

Example:

await clockodo.getService({ id: 10 });

getServices()

Get list of all services

Example:

await clockodo.getServices();

getTeam()

Get team by id.

Example:

await clockodo.getTeam({ id: 10 });

getTeams()

Get list of all teams.

Example:

await clockodo.getTeams();

getLumpSumService()

Get a lumpsum service by its ID.

Example:

await clockodo.getLumpSumService({ id: 10 });

getLumpSumServices()

Get a list of all lumpsum services

Example:

await clockodo.getLumpSumServices();

getTargethoursRow()

Get a specific target hour period for a specific user by its ID (not the ID of the user)

Example:

await clockodo.getTargethoursRow({ id: 1234 });

getTargethours()

Get list of target hours for all users, with option to pass an object with an usersId to filter the history of target hours to a specific user.

Example:

await clockodo.getTargethours();
// or
await clockodo.getTargethours({ usersId: 346923 });

getUser()

Get a co-worker by their ID.

Example:

await clockodo.getUser({ id: 1263 });

getUsers()

Get list of users

Example:

await clockodo.getUsers();

getUserReport()

Get a co-worker by their ID.

Example:

await clockodo.getUserReport({ usersId: 1263, year: 2017 });

getUserReports()

Get an employee/user's report, which contains data such as hours worked and holidays taken.

Example:

await clockodo.getUserReports({ year: 2017, type: 1 });

With this resource you can read all nonbusiness groups. The editing and adding of nonbusiness groups is currently not possible.

Example:

await clockodo.getNonbusinessGroups();

With this resource you can read all nonbusiness days. The editing and adding of nonbusiness days is currently not possible.

Example:

await clockodo.getNonbusinessDays({
  nonbusinessgroupsId: 123,
  year: 2021,
});

With this resource you can read user and company seetings for the logged in user. Editing is currently not possible.

Example:

await clockodo.getAggregatesUsersMe();

Post Methods

addAbsence()

Default behavior adds an absence for the user attached to the credentials given to the clockodo object. To add the absence for another user you can use the usersId option if you have the permissions.

Example:

import { AbsenceType } from "clockodo";

await clockodo.addAbsence({
  dateSince: "2017-08-18T00:00:00Z",
  dateUntil: "2018-02-09T00:00:00Z",
  type: AbsenceType.SpecialLeave,
  note: "elternzeit",
  usersId: 12321,
});

addCustomer()

Adds a customer to the organization.

Example:

await clockodo.addCustomer({ name: "Weyland-Yutani" });

addEntry()

Creates an entry for either the user attached to the Clockodo instance or the passed in usersId. Depending on the type of entry different properties are required:

Type of entry Required properties
Manual time entry customersId, servicesId, billable, timeSince, timeUntil
Lumpsum value entry customersId, servicesId, billable, timeSince, lumpsum
Lumpsum service entry customersId, lumpsumServicesAmount, lumpsumServicesId, billable, timeSince

Example:

import { Billability } from "clockodo";

await clockodo.addEntry({
  customersId: 1,
  servicesId: 2,
  billable: Billability.Billable,
  timeSince: "2018-10-01T00:00:00Z",
  timeUntil: "2018-10-01T03:00:00Z",
});

addProject()

Creates a project for an existing customer.

Example:

await clockodo.addProject({ name: "Clockodo Api Wrapper", customersId: 1 });

addService()

Adds to the list of services offered by your organization.

Example:

await clockodo.addService({ name: "Thinking" });

addTeam()

Creates a new team under your organization.

Example:

await clockodo.addTeam({ name: "Gold Team" });

addUser()

Creates new user in organization.

Example:

import { UserRole } from "clockodo";

await clockodo.addUser({
  name: "Merkel",
  number: "08",
  email: "[email protected]",
  role: UserRole.Owner,
});

startClock()

Start a new running clockodo entry.

Example:

import { Billability } from "clockodo";

await clockodo.startClock({
  customersId: 24,
  servicesId: 7,
  projectsId: 365,
  billable: Billability.Billable,
});

Put methods

changeClockDuration()

Changes the duration of an entry. Because the ID returned by clock methods is just the entry ID, and this function can only be used after an entry is finished, there seems to be no difference from using editEntry().

Example:

await clockodo.changeClockDuration({
  entriesId: 7082,
  duration: 540,
  durationBefore: 300,
});

editAbsence()

Edit existing Clockodo absence.

Example:

await clockodo.editAbsence({ id: 74, note: "I know what he did last summer" });

editCustomer()

Edit existing Clockodo customer.

Example:

await clockodo.editCustomer({ id: 15, name: "The Mystery Gang" });

editEntry()

Changes the values of a Clockodo entry. Unlike changeClockDuration(), editEntry() can seemingly mutate any of the accepted parameters even when the entry is running.

Example:

await clockodo.editEntry({ id: 365, duration: 540 });

editEntryGroup()

Allows for mass edit of entries based on a set of filters.

Example:

import { Billability } from "clockodo";

await clockodo.editEntryGroup({
  timeSince: "2017-08-18T00:00:00Z",
  timeUntil: "2018-02-09T00:00:00Z",
  filterText: "Browsing Reddit",
  billable: Billability.NotBillable,
});

editProject()

Edit existing project.

Example:

await clockodo.editProject({ id: 20, name: "Awesome new project" });

editService()

Edit existing service.

Example:

await clockodo.editService({ id: 23, name: "Room Service" });

editTeam()

Edit existing team.

Example:

await clockodo.editTeam({ id: 6324, name: "New Team Name" });

editUser()

Edit existing user.

Example:

await clockodo.editUser({ id: 33, name: "Moalo Loco" });

Delete methods

deleteCustomer()

Deletes the customer.

Example:

await clockodo.deleteCustomer({ id: 343 });

deleteProject()

Deletes the project.

Example:

await clockodo.deleteProject({ id: 8 });

deleteService()

Deletes the service.

Example:

await clockodo.deleteService({ id: 94 });

deleteUser()

Deletes user.

Example:

await clockodo.deleteUser({ id: 7 });

deleteAbsence()

Deletes absence (go figure).

Example:

await clockodo.deleteAbsence({ id: 31 });

deleteEntry()

Deletes a single entry by ID

Example:

await clockodo.deleteEntry({ id: 543512 });

deleteTeam()

Deletes a team by ID

Example:

await clockodo.deleteTeam({ id: 764 });

deleteEntryGroup()

Deletes one or more entries based on a series of filters that builds an "entry group".

Example:

await clockodo.deleteEntryGroup({
  timeSince: "2017-08-18T00:00:00Z",
  timeUntil: "2018-02-09T00:00:00Z",
  text: "chilin everyday",
});

register()

Creates a new clockodo account.

Example:

await clockodo.register({
  companiesName: "Acme Corporation",
  name: "Road Runner",
  email: "[email protected]",
});

stopClock()

Stops a running clock/entry.

Example for self:

await clockodo.stopClock({ entriesId: 7082 });

Example for another user (needs requesting user to be owner):

await clockodo.stopClock({ entriesId: 7082, usersId: 123 });

Development

To run integration tests you need to create an .env by copying the .env.example and entering credentials of a dev-user, as you don't want to mess up your real clockodo data.

License

MIT

Sponsors

clockodo's People

Contributors

anki247 avatar aubcel avatar borisovskip avatar cegento avatar dependabot[bot] avatar dsumer avatar gaetancollaud avatar jannikkeye avatar jek91 avatar jhnns avatar kw-clickbits avatar mrclickbits avatar que-tin avatar sbat avatar semantic-release-bot avatar tannerbaum avatar vpclockodo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

clockodo's Issues

lumpSum is missing in mapKeys

lumpSum is used for non-unit-lump sums. Currently only unit-lump-sums will be mapped correctly, since they use lumpSums_id and lumpSums_amount.

Passing an array as a parameter causes a 400

Discovered by @meaku when attempting to use the API like so:

const entries = await clockodo.getEntryGroups(
        {
            timeSince: startTime,
            timeUntil: endTime,
            grouping: ["projects_id"]
        },
        {
           filterCustomerId: [60928, 514726]
        },
    );

This leads to the filters looking like filter[customers_id][]=60928&filter[customers_id][]=514726, which is the format required for grouping in the getEntryGroups endpoint but not here. So we need to find a way to fix that.

Also fun fact, I am using the node qs library thinking it was https://www.npmjs.com/package/query-string. Surprising we got this far without noticing that...

Improve filter interface

There are two issues with the current filter interface (that is used by getEntries for instance):

  • We need to maintain a separate query param mapping
  • The return type of a filtered response is not accurate. If I call getEntries() with a filter on customersId, I'd expect the returned filter property to only have customersId.

I'd like to have an interface like this:

const response = await clockodo.getEntries({
   filter: {
      customersId: 123
   }
});

// response is now a type that looks like this

type Response = {
    paging: Paging
    filter: {
        customersId: number
    },
    entries: Array<Entry>
}

This allows us to type the filter for each endpoint. This should be doable with generics.

Settings based switch clock billability [feature request (or bug report)]

Hello,
currently the clockodo rest API supports a small but cool feature in which you can have billability based on your projects settings, by omitting the billable key (in the api request) from the parameters. (https://www.clockodo.com/en/api/clock/#c15189-headline).

If i do the same thing with the javascript clockodo api, i get the following error: Unhandled error: Error: Missing required parameter "billable" at Proxy.checkRequired

The request is currently done like in this example:

    const parameters = {
        customersId: customersId,
        projectsId: projectsId,
        servicesId: servicesId,
        text: text,
        usersId: usersId
    }
    if (unbillable) {
        parameters.billable = 0;
    }
    await clockodoApi.startClock(parameters);

If now the variable unbillable is false [to get the projects settings billability], the field billable will be undefined which causes the above mentioned error. I also don't know whether i am doing a mistake, or whether the feature is not implemented yet.

Thanks! :)

addEntry doesn't work

The function parameters are slightly off which makes the function unusable. customerId should be customersId and serviceId should be servicesId.

Introduce optional caching fo requests

The Clockodo Api has some very expencive requests e.g. getUserReport.
In Checkodo there are multiple functions using these expensive requests.
This results in multiple simliar requests for loading the dashboard for example. To reduce these redundant requests it would be good to add an optional cache behavior to the Clockodo SDK

Creating a Clockodo instance with cache should look like that:

const clockodo = new Clockodo({
    apiKey: "key",
    user: "usermail",
    cacheTime: 15 * 60 * 1000      //time of the cache in ms, here it would be 15 minutes
});
  • The chache entry is valid as long as there wasn't a PUT, POST, PATCH or DELETE request to the same url.
  • When one of those requests is made or the time is over there will be made a new request which response will be stored in the cache again.
  • If the instance is created without cacheTime it should behave the same as before.

Switch to GitHub actions for CI

I would like to migrate our Open Source modules to GitHub actions for integration tests.

We should add our integration test account as secrets so that we can also run integration tests

Follow up of #35

Remove CommonJS build

As soon as one of our dependencies switches to ESM-only, we will also need to switch to ESM-only. This already happened in the past when updating of one of sindre's packages (who switched to ESM-only).

Improve parameter strictness

The Clockodo SDK has been previously designed in a way to allow additional parameters that haven't been typed yet. E.g. imagine there is a new parameter we haven't added to the function type yet:

clockodo.getUser({
  newParam: true
});

In TypeScript, this would show an error because of the "excess property check". We extend all params with Record<string, unknown> to allow new, untyped params.

As a downside, this also makes it possible to missspell properties and TypeScript will allow it:

clockodo.getUser({
  // TS allows this because it thinks it's a new param
  oldButMisspelledParam: true
});

I think, we should remove & Record<string, unknown> thus making the type check more strict. As a downside, the SDK will now show an error if a new, untyped param is used. However, I think this can be easily fixed:

clockodo.getUser({
  // New, untyped param. Remove this once it got added to the SDK
  // @ts-expect-error
  newParam: true
});

This way, it will show a TypeScript error as soon as the type is added and the ts-expect-error is not necessary anymore. With us working more actively on the SDK, I think we should try to get the complete API typed and up-to-date.

What do you think @mrclickbits @anki247 @dsumer ?

Reconsider Method Parameters

Currently with some endpoint methods that have a required ID, we have it as a separate argument from the typical object passed in with all the other parameters(required or not). Example:

separate

However, others with required parameters are lumped together in a single object:

object

Should we make required parameters separate, or keep them as is?

Missing SearchTexts endpoint method

Needs to be added to api.js. Would also be nice if a test was written for it in api.integration.tests.js.

screen shot 2018-04-05 at 14 46 03

You will need to wait for #1 where the other endpoint methods are defined.

Support email addresses with special characters

In order to authenticate via API key, we need to add the email address as HTTP header. If that email address contains a special character, there might be problems authenticating.

We should:

  • check if that is handled by axios
  • write a test to verify that it will work in the future (e.g. when we remove axios #80)

Add Remaining Post Endpoints

Currently we only have one Post endpoint in the module. Now that this package is published, we should make it a priority to map out the remaining endpoints. I will start with POST.

Improve Sample Response Data in README

Right now I just copied the response structure found on Clockodo's API. I think it would be a lot more helpful to show the actual key/value pairs that are returned in the response data.

Fix ESLint

As we changed clockodo to typescript, we need to adjust the eslintrc.js.

  • Add peerigon/typescript to use the matching eslint rules.
  • Edit tsconfig.js and create new one in src folder
  • Fix tests and linting errors

Async methods should not throw

It's good practice that async methods (async function or functions that return a promise) do never throw. They should reject the promise. Otherwise, it leaves the consumers of our lib in a situation where they need to do things like:

try {
clockodo
    .doSomething()
    .then(continueSomething)
    .catch(console.error); // catch the async error
} catch (err) {
    console.error(err); // also catch the sync error
}

You get the gist :)

Since we don't have async functions in our api.js and we're throwing on _checkRequired, our API wrapper will throw an error (instead of rejecting the promise).

Could you rewrite the methods to async functions instead? Async functions reject the promise automatically. The refactoring shouldn't be very hard.

Doesn't work for Turkish users

The camelcase-keys package uses toLocaleUpperCase() internally which uses the browser's locale to transform object keys.

The Turkish language, however, has surprising capitalization rules:

console.log(city.toLocaleUpperCase('en-US'));
// expected output: "ISTANBUL"

console.log(city.toLocaleUpperCase('TR'));
// expected output: "İSTANBUL"

This leads to the problem that for Turkish users, entries are transformed into this:

            "customersİd": 619336,
            "projectsİd": 631365,
            "usersİd": 62488,

Refactor public API

The current public API has been designed without TypeScript in mind. Back in the days we wanted to separate required from optional parameters. However, with TypeScript, this is not necessary anymore.

While this decision was ok for GET methods, I think it also leads to an awkward API when using POST and PUT because it requires you to split the entity like this:

// Instead of this...
clockodo.addEntry(entry);
// ... I have to do this
clockodo.addEntry({customerId: entry.customersId, serviceId: entry.servicesId, billable: entry.billable}, entry);

We should refactor the public API like this:

type ApiModelParam<Model, RequiredProps extends keyof Model> = Required<Pick<Model, RequiredProps>> &
    Partial<Omit<Model, RequiredProps>>;

export default class Clockodo {
    addEntry = async (entry: ApiModelParam<ApiEntry, 'customersId' | 'servicesId' | 'billable'>) => {
        /* ... */
    };

    getEntry = async (id: ApiEntry['id']) => {
        /* ... */
    };

    getEntries = async ({
        timeSince,
        timeUntil,
    }: {
        timeSince: ApiDateTime;
        timeUntil: ApiDateTime;
        /* plus optional properties... */
    }) => {
        /* ... */
    };

    editEntry = async (entry: ApiModelParam<ApiEntry, 'id'>) => {
        /* ... */
    };

    deleteEntry = async (id: ApiEntry['id']) => {
        /* ... */
    };
}
  • addEntry should be called with the entry model with the mandatory properties and everything else optional
  • getEntry just with an id
  • getEntries with a query object with some properties mandatory
  • editEntry with the entry model where the id is mandatory and everything else is optional
  • deleteEntry with just the id

I also think that we should return the whole response, not just the response body, because sometimes it's necessary to also get meta information about the response. The response should look similar to the Response object (although we can't use it exactly since the SDK should also work in Node.js where we don't have a full Response object.

The returned object should have the following properties:

  • headers
  • ok
  • status
  • body

Implement clockodo requirements

  • It should be possible to use cookie auth
  • It should be possible to configure the API endpoint -> #53
  • Refactor method params: Since we're using TypeScript, we don't need to split between required and optional parameters anymore. In a lot of instances, it's easier for the consumer to just use one method parameter (e.g. addEntry or editEntry).
  • Add API errors
  • We should get rid of the lodash dependency, the bundle is currently too big see https://bundlephobia.com/[email protected] -> partially solved in #55
  • We should not rename query parameters, only change the casing and remove special characters -> #53
  • There should be a modern build without polyfills
  • Make axios-cache-adapter optional -> #55
  • Harmonize entry type with Clockodo's internal entry type
  • Expose low-level get, post, put, delete with common error handling
  • Return response object (with statusCode, body and headers) instead just the body

Create interfaces for return types

Currently the generated typings specify Promise<any as return type for all functions as those are not specified. I think it would be beneficial to supply interfaces for the actual return values.

Doesn't really make sense to have typings if we don't supply these types in my opinion. This is of course a bit tedious as we'd have to create the interfaces by hand from the api docs and keep them up to date. Does that effort outweigh the benefit of having typed return types?

Edit: came upon this during checked typescript rewrite.

add planned hours

Clockodo will release in q3 the reading access to the planned hours. I will integrate it to the node api.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.