UNPKG

timeld-tiki

Version:

timeld - Tiki Timesheets connector

148 lines (136 loc) 4.96 kB
import setupFetch from '@zeit/fetch'; import { domainRelativeIri, lastPathComponent, ResultsReadable } from 'timeld-common'; import { each } from 'rx-flowable'; import LOG from 'loglevel'; /** * @typedef {object} TikiConfig * @property {string} api API URL e.g. https://timesheet.dev3.evoludata.com/api/trackers/2/items * @property {string} token OAuth2 token */ /** * @implements Connector */ export default class TikiConnector { // noinspection JSUnusedGlobalSymbols /** Used by the gateway to provide the configuration constructor parameter */ static configKey = 'tiki'; // noinspection JSUnusedGlobalSymbols /** Used by the gateway to negotiate content types */ static contentType = 'application/json+tiki'; /** * Construct with configuration parameters * @param {TikiConfig} config * @param {GraphSubject} ext * @param {import('@zeit/fetch').Fetch} fetch injected fetch */ constructor(config, ext, fetch = setupFetch()) { const missingConfig = ['api', 'token'].filter(k => !config[k]); if (missingConfig.length) throw new Error(`Missing Tiki config: ${missingConfig.join(', ')}`); const apiRoot = new URL(config.api); if (apiRoot.pathname.endsWith('/')) apiRoot.pathname = apiRoot.pathname.slice(0, -1); this.ext = ext; /** * @param {TimesheetTrackerItem} trackerItem * @returns {Promise<*>} */ this.post = async trackerItem => { let url = apiRoot; if (trackerItem.itemId != null) url += `/${trackerItem.itemId}`; const res = await fetch(url.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Bearer ${(config.token)}` }, method: 'POST', body: trackerItem.toString() }); if (!res.ok) throw new Error(`Fetch failed with ${res.statusText}, tracker item: ${JSON.stringify(trackerItem)}`); const body = await res.text(); if (body.length > 0) return JSON.parse(body); }; } async syncTimesheet(tsId, state) { if (state) { await each(state.read({ '@describe': '?entry', '@where': { '@id': '?entry', '@type': 'Entry' } }).consume, src => this.newTrackerItem(tsId, src)); } } entryUpdate(tsId, update, state) { // Look for new Entries, and IDs for which we already have a tracker item return Promise.all(update['@insert'].map(async src => { if (src['@id'] in this.ext) { // Load the whole state for the entry // noinspection JSCheckFunctionSignatures src = await state.get(src['@id']); await this.post(new TimesheetTrackerItem(tsId, src, this.ext[src['@id']])); } else if (src['@type'] === 'Entry') { await this.newTrackerItem(tsId, src); } })); } async newTrackerItem(tsId, src) { // Inserting timesheet tracker item const res = await this.post(new TimesheetTrackerItem(tsId, src)); // Store the item ID for the entry this.ext[src['@id']] = res['itemId']; LOG.debug('Posted tracker item', res['itemId'], 'from Entry', src['@id']); } reportTimesheet(tsId, state) { return new ResultsReadable(state.read({ '@describe': '?entry', '@where': { '@id': '?id', '@type': 'Entry' } }).consume, { stringify: src => new TimesheetTrackerItem(tsId, src).toString(), separator: '\n' }); } } class TimesheetTrackerItem { /** * @param {AccountOwnedId} tsId * @param {GraphSubject} src timeld Entry subject * @param {number} [itemId] Tracker item ID (for update only) */ constructor(tsId, src, itemId) { this.itemId = itemId; this.tsUser = lastPathComponent(src['vf:provider']['@id']); this.tsProject = tsId.toRelativeIri(); this.tsDescription = src['activity']; const start = new Date(src['start']['@value']); this.tsDate = this.tsStartTime = tikiTime(start); const duration = src['duration']; if (duration) { this.tsDuration = JSON.stringify({ hours: Math.floor(duration / 60), minutes: Math.floor(duration % 60) }); const end = new Date(start.getTime() + (duration * 60000)); this.tsEndTime = tikiTime(end); } this.tsTimeldID = domainRelativeIri(src['@id'], tsId.toDomain()); // Original source ID may be external this.tsSource = src['external']?.['@id'] || this.tsTimeldID; } toString() { const { tsUser, tsProject, tsDescription, tsStartTime, tsEndTime, tsDate, tsDuration, tsSource, tsTimeldID } = this; return Object.entries({ tsUser, tsProject, tsDescription, tsStartTime, tsEndTime, tsDate, tsDuration, tsSource, tsTimeldID }).filter(([, value]) => value != null) .map(([field, value]) => `fields[${encodeURIComponent(field)}]=${encodeURIComponent(value)}`) .join('&'); } } function tikiTime(date) { return `${Math.floor(date.getTime() / 1000)}`; }