diff --git a/docs/api/cozy-client/interfaces/manifest.FrequencyOptions.md b/docs/api/cozy-client/interfaces/manifest.FrequencyOptions.md new file mode 100644 index 0000000000..850e448b07 --- /dev/null +++ b/docs/api/cozy-client/interfaces/manifest.FrequencyOptions.md @@ -0,0 +1,55 @@ +[cozy-client](../README.md) / [manifest](../modules/manifest.md) / FrequencyOptions + +# Interface: FrequencyOptions<> + +[manifest](../modules/manifest.md).FrequencyOptions + +frequency options object + +## Properties + +### dayOfMonth + +• **dayOfMonth**: `number` + +day of the month + +*Defined in* + +[packages/cozy-client/src/models/manifest.js:329](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L329) + +*** + +### dayOfWeek + +• **dayOfWeek**: `number` + +day of the week + +*Defined in* + +[packages/cozy-client/src/models/manifest.js:330](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L330) + +*** + +### hours + +• **hours**: `number` + +hours + +*Defined in* + +[packages/cozy-client/src/models/manifest.js:331](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L331) + +*** + +### minutes + +• **minutes**: `number` + +minutes + +*Defined in* + +[packages/cozy-client/src/models/manifest.js:332](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L332) diff --git a/docs/api/cozy-client/interfaces/manifest.randomDayTimeResult.md b/docs/api/cozy-client/interfaces/manifest.randomDayTimeResult.md new file mode 100644 index 0000000000..f225830490 --- /dev/null +++ b/docs/api/cozy-client/interfaces/manifest.randomDayTimeResult.md @@ -0,0 +1,29 @@ +[cozy-client](../README.md) / [manifest](../modules/manifest.md) / randomDayTimeResult + +# Interface: randomDayTimeResult<> + +[manifest](../modules/manifest.md).randomDayTimeResult + +## Properties + +### hours + +• **hours**: `number` + +hours + +*Defined in* + +[packages/cozy-client/src/models/manifest.js:323](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L323) + +*** + +### minutes + +• **minutes**: `number` + +minutes + +*Defined in* + +[packages/cozy-client/src/models/manifest.js:324](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L324) diff --git a/docs/api/cozy-client/modules/manifest.md b/docs/api/cozy-client/modules/manifest.md index 9d3cb90c13..75f8b5a450 100644 --- a/docs/api/cozy-client/modules/manifest.md +++ b/docs/api/cozy-client/modules/manifest.md @@ -2,6 +2,11 @@ # Namespace: manifest +## Interfaces + +* [FrequencyOptions](../interfaces/manifest.FrequencyOptions.md) +* [randomDayTimeResult](../interfaces/manifest.randomDayTimeResult.md) + ## Variables ### ROLE_IDENTIFIER @@ -22,7 +27,7 @@ Legacy login fields declared by some konnectors *Defined in* -[packages/cozy-client/src/models/manifest.js:11](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L11) +[packages/cozy-client/src/models/manifest.js:21](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L21) ## Functions @@ -42,7 +47,59 @@ Legacy login fields declared by some konnectors *Defined in* -[packages/cozy-client/src/models/manifest.js:63](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L63) +[packages/cozy-client/src/models/manifest.js:73](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L73) + +*** + +### getCronFromFrequency + +▸ **getCronFromFrequency**(`frequency`, `options?`): `string` + +Build a cron string for given frequency with given options +See https://docs.cozy.io/en/cozy-stack/jobs/#cron-syntax + +*Parameters* + +| Name | Type | Description | +| :------ | :------ | :------ | +| `frequency` | `"hourly"` | `"daily"` | `"weekly"` | `"monthly"` | Frequency | +| `options` | [`FrequencyOptions`](../interfaces/manifest.FrequencyOptions.md) | - | + +*Returns* + +`string` + +* The cron definition for trigger + +*Defined in* + +[packages/cozy-client/src/models/manifest.js:280](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L280) + +*** + +### getCronFromKonnector + +▸ **getCronFromKonnector**(`konnector`, `startDate?`, `randomDayTimeFn?`): `string` + +Build a cron string for given konnector and from a given start date + +*Parameters* + +| Name | Type | Default value | Description | +| :------ | :------ | :------ | :------ | +| `konnector` | `IOCozyKonnector` | `undefined` | io.cozy.konnectors object | +| `startDate` | `Date` | `undefined` | start date | +| `randomDayTimeFn` | `Function` | `randomDayTime` | - | + +*Returns* + +`string` + +* The cron definition for trigger + +*Defined in* + +[packages/cozy-client/src/models/manifest.js:307](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L307) *** @@ -66,7 +123,7 @@ The key for the identifier field, example 'login' *Defined in* -[packages/cozy-client/src/models/manifest.js:161](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L161) +[packages/cozy-client/src/models/manifest.js:171](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L171) *** @@ -86,7 +143,33 @@ The key for the identifier field, example 'login' *Defined in* -[packages/cozy-client/src/models/manifest.js:67](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L67) +[packages/cozy-client/src/models/manifest.js:77](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L77) + +*** + +### randomDayTime + +▸ **randomDayTime**(`start?`, `end?`, `randomize?`): [`randomDayTimeResult`](../interfaces/manifest.randomDayTimeResult.md) + +Returns an hour of the day between two hours given in parameters + +*Parameters* + +| Name | Type | Default value | Description | +| :------ | :------ | :------ | :------ | +| `start` | `number` | `0` | minimal start hour | +| `end` | `number` | `1` | maximal end hour | +| `randomize` | `Function` | `undefined` | The function used to generate random values | + +*Returns* + +[`randomDayTimeResult`](../interfaces/manifest.randomDayTimeResult.md) + +Object containing two atributes : hours and minutes + +*Defined in* + +[packages/cozy-client/src/models/manifest.js:248](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L248) *** @@ -108,7 +191,7 @@ Normalize app manifest, retro-compatibility for old manifests *Defined in* -[packages/cozy-client/src/models/manifest.js:77](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L77) +[packages/cozy-client/src/models/manifest.js:87](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L87) *** @@ -132,7 +215,7 @@ sanitized categories *Defined in* -[packages/cozy-client/src/models/manifest.js:56](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L56) +[packages/cozy-client/src/models/manifest.js:66](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L66) *** @@ -156,4 +239,4 @@ Ensures that fields has at least one field with the role 'identifier' *Defined in* -[packages/cozy-client/src/models/manifest.js:130](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L130) +[packages/cozy-client/src/models/manifest.js:140](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/manifest.js#L140) diff --git a/packages/cozy-client/src/models/manifest.js b/packages/cozy-client/src/models/manifest.js index ee454b12d2..4cc508ceaf 100644 --- a/packages/cozy-client/src/models/manifest.js +++ b/packages/cozy-client/src/models/manifest.js @@ -5,6 +5,16 @@ import findKey from 'lodash/findKey' export const ROLE_IDENTIFIER = 'identifier' +const DAILY = 'daily' +const HOURLY = 'hourly' +const WEEKLY = 'weekly' +const MONTHLY = 'monthly' +const VALID_FREQUENCIES = [DAILY, HOURLY, WEEKLY, MONTHLY] + +const DEFAULT_FREQUENCY = WEEKLY +// By default konnectors are run at random hour between 12:00PM and 05:00AM. +const DEFAULT_TIME_INTERVAL = [0, 5] + /** * Legacy login fields declared by some konnectors */ @@ -226,3 +236,98 @@ const sanitizeFields = _flow([ sanitizeRequired, sanitizeEncrypted ]) + +/** + * Returns an hour of the day between two hours given in parameters + * + * @param {number} start minimal start hour + * @param {number} end maximal end hour + * @param {function} randomize The function used to generate random values + * @returns {randomDayTimeResult} Object containing two atributes : hours and minutes + */ +export const randomDayTime = ( + start = 0, + end = 1, + randomize = (min, max) => Math.random() * (max - min) + min +) => { + if (typeof start !== 'number') + throw new Error('Parameter start must be a number') + if (typeof end !== 'number') throw new Error('Parameter end must be a number') + + if (typeof randomize !== 'function') + throw new Error('Parameter randomize must be a function') + + if (start < 0 || end > 24) throw new Error('interval must be inside [0, 24]') + + const r = randomize(start, end) + const hours = Math.floor(r) + const minutes = Math.floor((r - hours) * 60) + + if (hours < 0 || hours > 23) + throw new Error('randomize function returns invalid hour value') + + return { hours, minutes } +} + +/** + * Build a cron string for given frequency with given options + * See https://docs.cozy.io/en/cozy-stack/jobs/#cron-syntax + * + * @param {'hourly'|'daily'|'weekly'|'monthly'} frequency Frequency + * @param {FrequencyOptions} [options] - FrequencyOptions object + * @returns {String} - The cron definition for trigger + */ +export const getCronFromFrequency = (frequency, options = {}) => { + const { dayOfMonth = 1, dayOfWeek = 1, hours = 0, minutes = 0 } = options + const sanitizedFrequency = VALID_FREQUENCIES.includes(frequency) + ? frequency + : DEFAULT_FREQUENCY + + switch (sanitizedFrequency) { + case DAILY: + return `0 ${minutes} ${hours} * * *` + case HOURLY: + return `0 ${minutes} * * * *` + case MONTHLY: + return `0 ${minutes} ${hours} ${dayOfMonth} * *` + default: + // also WEEKLY + return `0 ${minutes} ${hours} * * ${dayOfWeek}` + } +} + +/** + * Build a cron string for given konnector and from a given start date + * + * @param {import('../types').IOCozyKonnector} konnector - io.cozy.konnectors object + * @param {Date} startDate - start date + * @param {function} [randomDayTimeFn] - function generating random hours and minutes + * @returns {String} - The cron definition for trigger + */ +export const getCronFromKonnector = ( + konnector, + startDate = new Date(), + randomDayTimeFn = randomDayTime +) => + getCronFromFrequency(konnector.frequency, { + ...randomDayTimeFn.apply( + null, + konnector.time_interval || DEFAULT_TIME_INTERVAL + ), + dayOfWeek: startDate.getDay(), + dayOfMonth: startDate.getDate() + }) + +/** + * @typedef {object} randomDayTimeResult + * @property {number} hours - hours + * @property {number} minutes - minutes + */ + +/** + * @typedef {object} FrequencyOptions - frequency options object + * @property {Number} [dayOfMonth] - day of the month + * @property {Number} [dayOfWeek] - day of the week + * @property {Number} [hours] - hours + * @property {Number} [minutes] - minutes + */ diff --git a/packages/cozy-client/src/models/manifest.spec.js b/packages/cozy-client/src/models/manifest.spec.js index 5947c40275..cf29aa91eb 100644 --- a/packages/cozy-client/src/models/manifest.spec.js +++ b/packages/cozy-client/src/models/manifest.spec.js @@ -1,7 +1,10 @@ import { sanitizeCategories, sanitize as sanitizeManifest, - getIdentifier + getIdentifier, + getCronFromFrequency, + getCronFromKonnector, + randomDayTime } from './manifest' describe('sanitizeCategories', () => { @@ -444,3 +447,136 @@ describe('getIdentifier', () => { expect(getIdentifier({})).toBe(null) }) }) + +describe('getCronFromFrequency', () => { + const options = { dayOfMonth: 25, dayOfWeek: 4, hours: 14, minutes: 15 } + it('creates default cron (weekly)', () => { + expect(getCronFromFrequency()).toEqual('0 0 0 * * 1') + }) + it('creates weekly cron', () => { + expect(getCronFromFrequency('weekly', options)).toEqual('0 15 14 * * 4') + }) + it('creates monthly cron', () => { + expect(getCronFromFrequency('monthly', options)).toEqual('0 15 14 25 * *') + }) + it('creates daily cron', () => { + expect(getCronFromFrequency('daily', options)).toEqual('0 15 14 * * *') + }) + it('creates hourly cron', () => { + expect(getCronFromFrequency('hourly', options)).toEqual('0 15 * * * *') + }) +}) + +describe('getCronFromKonnector', () => { + const randomDayTimeMock = jest.fn() + + beforeEach(() => { + randomDayTimeMock.mockImplementation((min, max) => ({ + hours: max - 1, + minutes: 59 + })) + }) + afterEach(() => { + randomDayTimeMock.mockReset() + }) + it('returns expected default cron', () => { + const konnector = {} + const date = new Date('2019-02-07T14:12:00') + expect(getCronFromKonnector(konnector, date, randomDayTimeMock)).toEqual( + `0 59 4 * * 4` + ) + }) + it('returns expected monthly cron', () => { + const konnector = { + frequency: 'monthly' + } + const date = new Date('2019-02-07T14:12:00') + expect(getCronFromKonnector(konnector, date, randomDayTimeMock)).toEqual( + `0 59 4 7 * *` + ) + }) + it('returns expected cron with time interval', () => { + const konnector = { + time_interval: [0, 12] + } + const date = new Date('2019-02-07T14:12:00') + expect(getCronFromKonnector(konnector, date, randomDayTimeMock)).toEqual( + `0 59 11 * * 4` + ) + }) +}) + +describe('daytime library', () => { + describe('randomDayTime', () => { + it('throws error on inconsistent start hour', () => { + expect(() => randomDayTime(-1, 12)).toThrow( + 'interval must be inside [0, 24]' + ) + }) + it('throws error on inconsistent end hour', () => { + expect(() => randomDayTime(2, 26)).toThrow( + 'interval must be inside [0, 24]' + ) + }) + it('throws error when randomize is null', () => { + expect(() => randomDayTime(0, 1, null)).toThrow( + 'Parameter randomize must be a function' + ) + }) + it('throws error when randomize is not a function', () => { + expect(() => randomDayTime(0, 1, 2)).toThrow( + 'Parameter randomize must be a function' + ) + }) + it('returns expected hours/minutes values', () => { + const randomizeStub = jest.fn().mockReturnValueOnce(10.58) + + const result = randomDayTime(0, 24, randomizeStub) + + expect(result).toEqual({ + hours: 10, + minutes: 34 + }) + }) + it('returns defaut hours/minutes with no parameter', () => { + // Test based on random function, not sure if it is a good idea, but it + // makes code 100% covered. + const result = randomDayTime() + + expect(result.hours).toBe(0) + + expect(result.minutes).toBeGreaterThanOrEqual(0) + expect(result.minutes).toBeLessThanOrEqual(59) + }) + it('returns valid hours/minutes with default randomize function', () => { + // Test based on random function, not sure if it is a good idea, but it + // makes code 100% covered. + const result = randomDayTime(19, 21) + + expect(result.hours).toBeGreaterThanOrEqual(19) + expect(result.hours).toBeLessThanOrEqual(20) + + expect(result.minutes).toBeGreaterThanOrEqual(0) + expect(result.minutes).toBeLessThanOrEqual(59) + }) + it('throw error on incorrect minimal hour', () => { + const randomizeStub = jest.fn().mockReturnValueOnce(-1) + + expect(() => randomDayTime(0, 24, randomizeStub)).toThrow( + 'randomize function returns invalid hour value' + ) + }) + it('throws error on incorrect maximal hour', () => { + const randomizeStub = jest.fn().mockReturnValueOnce(24) + + expect(() => randomDayTime(0, 24, randomizeStub)).toThrow( + 'randomize function returns invalid hour value' + ) + }) + it('handles floats', () => { + const randomizeStub = jest.fn().mockReturnValueOnce(10.58) + randomDayTime(5.5, 6.5, randomizeStub) + expect(randomizeStub).toHaveBeenCalledWith(5.5, 6.5) + }) + }) +}) diff --git a/packages/cozy-client/src/types.js b/packages/cozy-client/src/types.js index 971ee98755..9310d7ff39 100644 --- a/packages/cozy-client/src/types.js +++ b/packages/cozy-client/src/types.js @@ -33,6 +33,8 @@ import { QueryDefinition } from './queries/dsl' * @property {String} slug - slug of konnector * @property {ManifestFields} fields - konnector fields * @property {Boolean} clientSide - whether the konnector runs on client or not + * @property {'hourly'|'daily'|'weekly'|'monthly'} frequency - frequency at which the konnector is supposed to be run + * @property {Array} time_interval - interval of hours in the day where the konnector can be run * @typedef {CozyClientDocument & KonnectorsDocument} IOCozyKonnector - An io.cozy.konnectors document */ diff --git a/packages/cozy-client/types/models/manifest.d.ts b/packages/cozy-client/types/models/manifest.d.ts index 003a864753..564aee3e18 100644 --- a/packages/cozy-client/types/models/manifest.d.ts +++ b/packages/cozy-client/types/models/manifest.d.ts @@ -21,3 +21,37 @@ export const ROLE_IDENTIFIER: "identifier"; export const legacyLoginFields: string[]; export function sanitizeIdentifier(fields: import('../types').ManifestFields): import('../types').ManifestFields; export function getIdentifier(fields?: import('../types').ManifestFields): string | null; +export function randomDayTime(start?: number, end?: number, randomize?: Function): randomDayTimeResult; +export function getCronFromFrequency(frequency: 'hourly' | 'daily' | 'weekly' | 'monthly', options?: FrequencyOptions): string; +export function getCronFromKonnector(konnector: import('../types').IOCozyKonnector, startDate?: Date, randomDayTimeFn?: Function): string; +export type randomDayTimeResult = { + /** + * - hours + */ + hours: number; + /** + * - minutes + */ + minutes: number; +}; +/** + * - frequency options object + */ +export type FrequencyOptions = { + /** + * - day of the month + */ + dayOfMonth?: number; + /** + * - day of the week + */ + dayOfWeek?: number; + /** + * - hours + */ + hours?: number; + /** + * - minutes + */ + minutes?: number; +}; diff --git a/packages/cozy-client/types/types.d.ts b/packages/cozy-client/types/types.d.ts index 12aabd77ed..1d52af22c2 100644 --- a/packages/cozy-client/types/types.d.ts +++ b/packages/cozy-client/types/types.d.ts @@ -54,6 +54,14 @@ export type KonnectorsDocument = { * - whether the konnector runs on client or not */ clientSide: boolean; + /** + * - frequency at which the konnector is supposed to be run + */ + frequency: 'hourly' | 'daily' | 'weekly' | 'monthly'; + /** + * - interval of hours in the day where the konnector can be run + */ + time_interval: Array; }; /** * - An io.cozy.konnectors document