mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[8.16] [ResponseOps] [Alerting] Handle invalid RRule params and prevent infinite looping (#205650) (#205831)
# Backport This will backport the following commits from `main` to `8.16`: - [[ResponseOps] [Alerting] Handle invalid RRule params and prevent infinite looping (#205650)](https://github.com/elastic/kibana/pull/205650) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Zacqary Adam Xeper","email":"Zacqary@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-01-07T19:32:43Z","message":"[ResponseOps] [Alerting] Handle invalid RRule params and prevent infinite looping (#205650)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/205558\r\n\r\nUpdates the RRule library to correctly handle some scenarios with\r\ninvalid parameters that would either cause it to return strange\r\nrecurrence data or to infinitely loop. Specifically:\r\n\r\n- On `RRule` object creation, removes and ignores any `bymonth`,\r\n`bymonthday`, `byweekday`, or `byyearday` value that's out of bounds,\r\ne.g. less than 0 or greater than the number of possible months, days,\r\nweekdays, etc.\r\n- Successfully ignores cases of `BYMONTH=2, BYMONTHDAY=30` (February\r\n30th), an input that's complicated to invalidate but still won't ever\r\noccur\r\n\r\nAllowing these values to go unhandled led to unpredictable behavior. The\r\nRRule library uses Moment.js to compare dates, but Moment.js months,\r\ndays, and other values generally start at `0` while RRule values start\r\nat `1`. That led to several circumstances where we passed Moment.js a\r\nvalue of `-1`, which Moment.js interpreted as moving to the\r\n***previous*** year, month, or other period of time.\r\n\r\nAt worst, this could cause an infinite loop because the RRule library\r\nwas constantly iterating through the wrong year, never reaching the date\r\nit was supposed to end on.\r\n\r\nIn addition to making the RRule library more able to handle these cases,\r\nthis PR also gives it a hard 100,000 iteration limit to prevent any\r\npossible infinite loops we've missed.\r\n\r\nLastly, the Snooze Schedule APIs also come with additional validation to\r\nhopefully prevent out of bounds dates from ever being set.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com>\r\nCo-authored-by: Janki Salvi <jankigaurav.salvi@elastic.co>\r\nCo-authored-by: adcoelho <antonio.coelho@elastic.co>","sha":"b30210929be0824f684f0b7d9d13bc936c1cbd22","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:ResponseOps","v9.0.0","Feature:Alerting/RulesFramework","backport:version","v8.18.0","v8.16.3","v8.17.1"],"number":205650,"url":"https://github.com/elastic/kibana/pull/205650","mergeCommit":{"message":"[ResponseOps] [Alerting] Handle invalid RRule params and prevent infinite looping (#205650)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/205558\r\n\r\nUpdates the RRule library to correctly handle some scenarios with\r\ninvalid parameters that would either cause it to return strange\r\nrecurrence data or to infinitely loop. Specifically:\r\n\r\n- On `RRule` object creation, removes and ignores any `bymonth`,\r\n`bymonthday`, `byweekday`, or `byyearday` value that's out of bounds,\r\ne.g. less than 0 or greater than the number of possible months, days,\r\nweekdays, etc.\r\n- Successfully ignores cases of `BYMONTH=2, BYMONTHDAY=30` (February\r\n30th), an input that's complicated to invalidate but still won't ever\r\noccur\r\n\r\nAllowing these values to go unhandled led to unpredictable behavior. The\r\nRRule library uses Moment.js to compare dates, but Moment.js months,\r\ndays, and other values generally start at `0` while RRule values start\r\nat `1`. That led to several circumstances where we passed Moment.js a\r\nvalue of `-1`, which Moment.js interpreted as moving to the\r\n***previous*** year, month, or other period of time.\r\n\r\nAt worst, this could cause an infinite loop because the RRule library\r\nwas constantly iterating through the wrong year, never reaching the date\r\nit was supposed to end on.\r\n\r\nIn addition to making the RRule library more able to handle these cases,\r\nthis PR also gives it a hard 100,000 iteration limit to prevent any\r\npossible infinite loops we've missed.\r\n\r\nLastly, the Snooze Schedule APIs also come with additional validation to\r\nhopefully prevent out of bounds dates from ever being set.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com>\r\nCo-authored-by: Janki Salvi <jankigaurav.salvi@elastic.co>\r\nCo-authored-by: adcoelho <antonio.coelho@elastic.co>","sha":"b30210929be0824f684f0b7d9d13bc936c1cbd22"}},"sourceBranch":"main","suggestedTargetBranches":["8.16","8.17"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/205650","number":205650,"mergeCommit":{"message":"[ResponseOps] [Alerting] Handle invalid RRule params and prevent infinite looping (#205650)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/205558\r\n\r\nUpdates the RRule library to correctly handle some scenarios with\r\ninvalid parameters that would either cause it to return strange\r\nrecurrence data or to infinitely loop. Specifically:\r\n\r\n- On `RRule` object creation, removes and ignores any `bymonth`,\r\n`bymonthday`, `byweekday`, or `byyearday` value that's out of bounds,\r\ne.g. less than 0 or greater than the number of possible months, days,\r\nweekdays, etc.\r\n- Successfully ignores cases of `BYMONTH=2, BYMONTHDAY=30` (February\r\n30th), an input that's complicated to invalidate but still won't ever\r\noccur\r\n\r\nAllowing these values to go unhandled led to unpredictable behavior. The\r\nRRule library uses Moment.js to compare dates, but Moment.js months,\r\ndays, and other values generally start at `0` while RRule values start\r\nat `1`. That led to several circumstances where we passed Moment.js a\r\nvalue of `-1`, which Moment.js interpreted as moving to the\r\n***previous*** year, month, or other period of time.\r\n\r\nAt worst, this could cause an infinite loop because the RRule library\r\nwas constantly iterating through the wrong year, never reaching the date\r\nit was supposed to end on.\r\n\r\nIn addition to making the RRule library more able to handle these cases,\r\nthis PR also gives it a hard 100,000 iteration limit to prevent any\r\npossible infinite loops we've missed.\r\n\r\nLastly, the Snooze Schedule APIs also come with additional validation to\r\nhopefully prevent out of bounds dates from ever being set.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com>\r\nCo-authored-by: Janki Salvi <jankigaurav.salvi@elastic.co>\r\nCo-authored-by: adcoelho <antonio.coelho@elastic.co>","sha":"b30210929be0824f684f0b7d9d13bc936c1cbd22"}},{"branch":"8.x","label":"v8.18.0","labelRegex":"^v8.18.0$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/205803","number":205803,"state":"MERGED","mergeCommit":{"sha":"a02fcb232faed2f385ce9b97fbdb323ccbf8ca45","message":"[8.x] [ResponseOps] [Alerting] Handle invalid RRule params and prevent infinite looping (#205650) (#205803)\n\n# Backport\n\nThis will backport the following commits from `main` to `8.x`:\n- [[ResponseOps] [Alerting] Handle invalid RRule params and prevent\ninfinite looping\n(#205650)](https://github.com/elastic/kibana/pull/205650)\n\n<!--- Backport version: 9.4.3 -->\n\n### Questions ?\nPlease refer to the [Backport tool\ndocumentation](https://github.com/sqren/backport)\n\n<!--BACKPORT [{\"author\":{\"name\":\"Zacqary Adam\nXeper\",\"email\":\"Zacqary@users.noreply.github.com\"},\"sourceCommit\":{\"committedDate\":\"2025-01-07T19:32:43Z\",\"message\":\"[ResponseOps]\n[Alerting] Handle invalid RRule params and prevent infinite looping\n(#205650)\\n\\n## Summary\\r\\n\\r\\nCloses\nhttps://github.com/elastic/kibana/issues/205558\\r\\n\\r\\nUpdates the RRule\nlibrary to correctly handle some scenarios with\\r\\ninvalid parameters\nthat would either cause it to return strange\\r\\nrecurrence data or to\ninfinitely loop. Specifically:\\r\\n\\r\\n- On `RRule` object creation,\nremoves and ignores any `bymonth`,\\r\\n`bymonthday`, `byweekday`, or\n`byyearday` value that's out of bounds,\\r\\ne.g. less than 0 or greater\nthan the number of possible months, days,\\r\\nweekdays, etc.\\r\\n-\nSuccessfully ignores cases of `BYMONTH=2, BYMONTHDAY=30`\n(February\\r\\n30th), an input that's complicated to invalidate but still\nwon't ever\\r\\noccur\\r\\n\\r\\nAllowing these values to go unhandled led to\nunpredictable behavior. The\\r\\nRRule library uses Moment.js to compare\ndates, but Moment.js months,\\r\\ndays, and other values generally start\nat `0` while RRule values start\\r\\nat `1`. That led to several\ncircumstances where we passed Moment.js a\\r\\nvalue of `-1`, which\nMoment.js interpreted as moving to the\\r\\n***previous*** year, month, or\nother period of time.\\r\\n\\r\\nAt worst, this could cause an infinite loop\nbecause the RRule library\\r\\nwas constantly iterating through the wrong\nyear, never reaching the date\\r\\nit was supposed to end on.\\r\\n\\r\\nIn\naddition to making the RRule library more able to handle these\ncases,\\r\\nthis PR also gives it a hard 100,000 iteration limit to\nprevent any\\r\\npossible infinite loops we've missed.\\r\\n\\r\\nLastly, the\nSnooze Schedule APIs also come with additional validation\nto\\r\\nhopefully prevent out of bounds dates from ever being\nset.\\r\\n\\r\\n### Checklist\\r\\n\\r\\n- [x] [Unit or\nfunctional\\r\\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\\r\\nwere\nupdated or added to match the most common\nscenarios\\r\\n\\r\\n---------\\r\\n\\r\\nCo-authored-by: kibanamachine\n<42973632+kibanamachine@users.noreply.github.com>\\r\\nCo-authored-by:\nJanki Salvi\n<117571355+js-jankisalvi@users.noreply.github.com>\\r\\nCo-authored-by:\nJanki Salvi <jankigaurav.salvi@elastic.co>\\r\\nCo-authored-by: adcoelho\n<antonio.coelho@elastic.co>\",\"sha\":\"b30210929be0824f684f0b7d9d13bc936c1cbd22\",\"branchLabelMapping\":{\"^v9.0.0$\":\"main\",\"^v8.18.0$\":\"8.x\",\"^v(\\\\d+).(\\\\d+).\\\\d+$\":\"$1.$2\"}},\"sourcePullRequest\":{\"labels\":[\"release_note:fix\",\"Team:ResponseOps\",\"v9.0.0\",\"Feature:Alerting/RulesFramework\",\"backport:version\",\"v8.18.0\",\"v8.16.3\",\"v8.17.1\"],\"title\":\"[ResponseOps]\n[Alerting] Handle invalid RRule params and prevent infinite\nlooping\",\"number\":205650,\"url\":\"https://github.com/elastic/kibana/pull/205650\",\"mergeCommit\":{\"message\":\"[ResponseOps]\n[Alerting] Handle invalid RRule params and prevent infinite looping\n(#205650)\\n\\n## Summary\\r\\n\\r\\nCloses\nhttps://github.com/elastic/kibana/issues/205558\\r\\n\\r\\nUpdates the RRule\nlibrary to correctly handle some scenarios with\\r\\ninvalid parameters\nthat would either cause it to return strange\\r\\nrecurrence data or to\ninfinitely loop. Specifically:\\r\\n\\r\\n- On `RRule` object creation,\nremoves and ignores any `bymonth`,\\r\\n`bymonthday`, `byweekday`, or\n`byyearday` value that's out of bounds,\\r\\ne.g. less than 0 or greater\nthan the number of possible months, days,\\r\\nweekdays, etc.\\r\\n-\nSuccessfully ignores cases of `BYMONTH=2, BYMONTHDAY=30`\n(February\\r\\n30th), an input that's complicated to invalidate but still\nwon't ever\\r\\noccur\\r\\n\\r\\nAllowing these values to go unhandled led to\nunpredictable behavior. The\\r\\nRRule library uses Moment.js to compare\ndates, but Moment.js months,\\r\\ndays, and other values generally start\nat `0` while RRule values start\\r\\nat `1`. That led to several\ncircumstances where we passed Moment.js a\\r\\nvalue of `-1`, which\nMoment.js interpreted as moving to the\\r\\n***previous*** year, month, or\nother period of time.\\r\\n\\r\\nAt worst, this could cause an infinite loop\nbecause the RRule library\\r\\nwas constantly iterating through the wrong\nyear, never reaching the date\\r\\nit was supposed to end on.\\r\\n\\r\\nIn\naddition to making the RRule library more able to handle these\ncases,\\r\\nthis PR also gives it a hard 100,000 iteration limit to\nprevent any\\r\\npossible infinite loops we've missed.\\r\\n\\r\\nLastly, the\nSnooze Schedule APIs also come with additional validation\nto\\r\\nhopefully prevent out of bounds dates from ever being\nset.\\r\\n\\r\\n### Checklist\\r\\n\\r\\n- [x] [Unit or\nfunctional\\r\\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\\r\\nwere\nupdated or added to match the most common\nscenarios\\r\\n\\r\\n---------\\r\\n\\r\\nCo-authored-by: kibanamachine\n<42973632+kibanamachine@users.noreply.github.com>\\r\\nCo-authored-by:\nJanki Salvi\n<117571355+js-jankisalvi@users.noreply.github.com>\\r\\nCo-authored-by:\nJanki Salvi <jankigaurav.salvi@elastic.co>\\r\\nCo-authored-by: adcoelho\n<antonio.coelho@elastic.co>\",\"sha\":\"b30210929be0824f684f0b7d9d13bc936c1cbd22\"}},\"sourceBranch\":\"main\",\"suggestedTargetBranches\":[\"8.x\",\"8.16\",\"8.17\"],\"targetPullRequestStates\":[{\"branch\":\"main\",\"label\":\"v9.0.0\",\"branchLabelMappingKey\":\"^v9.0.0$\",\"isSourceBranch\":true,\"state\":\"MERGED\",\"url\":\"https://github.com/elastic/kibana/pull/205650\",\"number\":205650,\"mergeCommit\":{\"message\":\"[ResponseOps]\n[Alerting] Handle invalid RRule params and prevent infinite looping\n(#205650)\\n\\n## Summary\\r\\n\\r\\nCloses\nhttps://github.com/elastic/kibana/issues/205558\\r\\n\\r\\nUpdates the RRule\nlibrary to correctly handle some scenarios with\\r\\ninvalid parameters\nthat would either cause it to return strange\\r\\nrecurrence data or to\ninfinitely loop. Specifically:\\r\\n\\r\\n- On `RRule` object creation,\nremoves and ignores any `bymonth`,\\r\\n`bymonthday`, `byweekday`, or\n`byyearday` value that's out of bounds,\\r\\ne.g. less than 0 or greater\nthan the number of possible months, days,\\r\\nweekdays, etc.\\r\\n-\nSuccessfully ignores cases of `BYMONTH=2, BYMONTHDAY=30`\n(February\\r\\n30th), an input that's complicated to invalidate but still\nwon't ever\\r\\noccur\\r\\n\\r\\nAllowing these values to go unhandled led to\nunpredictable behavior. The\\r\\nRRule library uses Moment.js to compare\ndates, but Moment.js months,\\r\\ndays, and other values generally start\nat `0` while RRule values start\\r\\nat `1`. That led to several\ncircumstances where we passed Moment.js a\\r\\nvalue of `-1`, which\nMoment.js interpreted as moving to the\\r\\n***previous*** year, month, or\nother period of time.\\r\\n\\r\\nAt worst, this could cause an infinite loop\nbecause the RRule library\\r\\nwas constantly iterating through the wrong\nyear, never reaching the date\\r\\nit was supposed to end on.\\r\\n\\r\\nIn\naddition to making the RRule library more able to handle these\ncases,\\r\\nthis PR also gives it a hard 100,000 iteration limit to\nprevent any\\r\\npossible infinite loops we've missed.\\r\\n\\r\\nLastly, the\nSnooze Schedule APIs also come with additional validation\nto\\r\\nhopefully prevent out of bounds dates from ever being\nset.\\r\\n\\r\\n### Checklist\\r\\n\\r\\n- [x] [Unit or\nfunctional\\r\\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\\r\\nwere\nupdated or added to match the most common\nscenarios\\r\\n\\r\\n---------\\r\\n\\r\\nCo-authored-by: kibanamachine\n<42973632+kibanamachine@users.noreply.github.com>\\r\\nCo-authored-by:\nJanki Salvi\n<117571355+js-jankisalvi@users.noreply.github.com>\\r\\nCo-authored-by:\nJanki Salvi <jankigaurav.salvi@elastic.co>\\r\\nCo-authored-by: adcoelho\n<antonio.coelho@elastic.co>\",\"sha\":\"b30210929be0824f684f0b7d9d13bc936c1cbd22\"}},{\"branch\":\"8.x\",\"label\":\"v8.18.0\",\"branchLabelMappingKey\":\"^v8.18.0$\",\"isSourceBranch\":false,\"state\":\"NOT_CREATED\"},{\"branch\":\"8.16\",\"label\":\"v8.16.3\",\"branchLabelMappingKey\":\"^v(\\\\d+).(\\\\d+).\\\\d+$\",\"isSourceBranch\":false,\"state\":\"NOT_CREATED\"},{\"branch\":\"8.17\",\"label\":\"v8.17.1\",\"branchLabelMappingKey\":\"^v(\\\\d+).(\\\\d+).\\\\d+$\",\"isSourceBranch\":false,\"state\":\"NOT_CREATED\"}]}]\nBACKPORT-->\n\nCo-authored-by: Zacqary Adam Xeper <Zacqary@users.noreply.github.com>"}},{"branch":"8.16","label":"v8.16.3","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.17","label":"v8.17.1","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> --------- Co-authored-by: Zacqary Adam Xeper <Zacqary@users.noreply.github.com>
This commit is contained in:
parent
d57842ab23
commit
98100987d6
9 changed files with 614 additions and 67 deletions
|
@ -7,6 +7,6 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { RRule, Frequency, Weekday } from './rrule';
|
||||
export type { Options } from './rrule';
|
||||
export declare type WeekdayStr = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
|
||||
export { RRule } from './rrule';
|
||||
export type { Options, WeekdayStr } from './types';
|
||||
export { Frequency, Weekday } from './types';
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
*/
|
||||
|
||||
import sinon from 'sinon';
|
||||
import { RRule, Frequency, Weekday } from './rrule';
|
||||
import { RRule } from './rrule';
|
||||
import { Frequency, Weekday } from './types';
|
||||
|
||||
const DATE_2019 = '2019-01-01T00:00:00.000Z';
|
||||
const DATE_2019_DECEMBER_19 = '2019-12-19T00:00:00.000Z';
|
||||
|
@ -730,6 +731,228 @@ describe('RRule', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
it('ignores invalid byweekday values', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2019_DECEMBER_19),
|
||||
freq: Frequency.WEEKLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
byweekday: [Weekday.TH, 0, -2],
|
||||
});
|
||||
expect(rule.all(14)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2019-12-19T00:00:00.000Z,
|
||||
2019-12-26T00:00:00.000Z,
|
||||
2020-01-02T00:00:00.000Z,
|
||||
2020-01-09T00:00:00.000Z,
|
||||
2020-01-16T00:00:00.000Z,
|
||||
2020-01-23T00:00:00.000Z,
|
||||
2020-01-30T00:00:00.000Z,
|
||||
2020-02-06T00:00:00.000Z,
|
||||
2020-02-13T00:00:00.000Z,
|
||||
2020-02-20T00:00:00.000Z,
|
||||
2020-02-27T00:00:00.000Z,
|
||||
2020-03-05T00:00:00.000Z,
|
||||
2020-03-12T00:00:00.000Z,
|
||||
2020-03-19T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
|
||||
const rule2 = new RRule({
|
||||
dtstart: new Date(DATE_2019),
|
||||
freq: Frequency.WEEKLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
byweekday: [Weekday.SA, Weekday.SU, Weekday.MO, 0],
|
||||
});
|
||||
|
||||
expect(rule2.all(9)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2019-01-05T00:00:00.000Z,
|
||||
2019-01-06T00:00:00.000Z,
|
||||
2019-01-07T00:00:00.000Z,
|
||||
2019-01-12T00:00:00.000Z,
|
||||
2019-01-13T00:00:00.000Z,
|
||||
2019-01-14T00:00:00.000Z,
|
||||
2019-01-19T00:00:00.000Z,
|
||||
2019-01-20T00:00:00.000Z,
|
||||
2019-01-21T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bymonth', () => {
|
||||
it('works with yearly frequency', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2019_DECEMBER_19),
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
bymonth: [2, 5],
|
||||
});
|
||||
expect(rule.all(14)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2020-02-19T00:00:00.000Z,
|
||||
2020-05-19T00:00:00.000Z,
|
||||
2021-02-19T00:00:00.000Z,
|
||||
2021-05-19T00:00:00.000Z,
|
||||
2022-02-19T00:00:00.000Z,
|
||||
2022-05-19T00:00:00.000Z,
|
||||
2023-02-19T00:00:00.000Z,
|
||||
2023-05-19T00:00:00.000Z,
|
||||
2024-02-19T00:00:00.000Z,
|
||||
2024-05-19T00:00:00.000Z,
|
||||
2025-02-19T00:00:00.000Z,
|
||||
2025-05-19T00:00:00.000Z,
|
||||
2026-02-19T00:00:00.000Z,
|
||||
2026-05-19T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
it('ignores invalid bymonth values', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2019_DECEMBER_19),
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
bymonth: [0],
|
||||
});
|
||||
expect(rule.all(14)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2019-12-19T00:00:00.000Z,
|
||||
2020-12-19T00:00:00.000Z,
|
||||
2021-12-19T00:00:00.000Z,
|
||||
2022-12-19T00:00:00.000Z,
|
||||
2023-12-19T00:00:00.000Z,
|
||||
2024-12-19T00:00:00.000Z,
|
||||
2025-12-19T00:00:00.000Z,
|
||||
2026-12-19T00:00:00.000Z,
|
||||
2027-12-19T00:00:00.000Z,
|
||||
2028-12-19T00:00:00.000Z,
|
||||
2029-12-19T00:00:00.000Z,
|
||||
2030-12-19T00:00:00.000Z,
|
||||
2031-12-19T00:00:00.000Z,
|
||||
2032-12-19T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bymonthday', () => {
|
||||
it('works with monthly frequency', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2019_DECEMBER_19),
|
||||
freq: Frequency.MONTHLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
bymonthday: [1, 15],
|
||||
});
|
||||
expect(rule.all(14)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2020-01-01T00:00:00.000Z,
|
||||
2020-01-15T00:00:00.000Z,
|
||||
2020-02-01T00:00:00.000Z,
|
||||
2020-02-15T00:00:00.000Z,
|
||||
2020-03-01T00:00:00.000Z,
|
||||
2020-03-15T00:00:00.000Z,
|
||||
2020-04-01T00:00:00.000Z,
|
||||
2020-04-15T00:00:00.000Z,
|
||||
2020-05-01T00:00:00.000Z,
|
||||
2020-05-15T00:00:00.000Z,
|
||||
2020-06-01T00:00:00.000Z,
|
||||
2020-06-15T00:00:00.000Z,
|
||||
2020-07-01T00:00:00.000Z,
|
||||
2020-07-15T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
it('ignores invalid bymonthday values', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2019_DECEMBER_19),
|
||||
freq: Frequency.MONTHLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
bymonthday: [0, -1, 32],
|
||||
});
|
||||
expect(rule.all(14)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2019-12-19T00:00:00.000Z,
|
||||
2020-01-19T00:00:00.000Z,
|
||||
2020-02-19T00:00:00.000Z,
|
||||
2020-03-19T00:00:00.000Z,
|
||||
2020-04-19T00:00:00.000Z,
|
||||
2020-05-19T00:00:00.000Z,
|
||||
2020-06-19T00:00:00.000Z,
|
||||
2020-07-19T00:00:00.000Z,
|
||||
2020-08-19T00:00:00.000Z,
|
||||
2020-09-19T00:00:00.000Z,
|
||||
2020-10-19T00:00:00.000Z,
|
||||
2020-11-19T00:00:00.000Z,
|
||||
2020-12-19T00:00:00.000Z,
|
||||
2021-01-19T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bymonth, bymonthday', () => {
|
||||
it('works with yearly frequency', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2019_DECEMBER_19),
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
bymonth: [2, 5],
|
||||
bymonthday: [8],
|
||||
});
|
||||
expect(rule.all(14)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2020-02-08T00:00:00.000Z,
|
||||
2020-05-08T00:00:00.000Z,
|
||||
2021-02-08T00:00:00.000Z,
|
||||
2021-05-08T00:00:00.000Z,
|
||||
2022-02-08T00:00:00.000Z,
|
||||
2022-05-08T00:00:00.000Z,
|
||||
2023-02-08T00:00:00.000Z,
|
||||
2023-05-08T00:00:00.000Z,
|
||||
2024-02-08T00:00:00.000Z,
|
||||
2024-05-08T00:00:00.000Z,
|
||||
2025-02-08T00:00:00.000Z,
|
||||
2025-05-08T00:00:00.000Z,
|
||||
2026-02-08T00:00:00.000Z,
|
||||
2026-05-08T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
it('ignores valid dates that do not exist e.g. February 30th', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2019_DECEMBER_19),
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
bymonth: [2, 5],
|
||||
bymonthday: [30],
|
||||
});
|
||||
expect(rule.all(14)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2020-05-30T00:00:00.000Z,
|
||||
2021-05-30T00:00:00.000Z,
|
||||
2022-05-30T00:00:00.000Z,
|
||||
2023-05-30T00:00:00.000Z,
|
||||
2024-05-30T00:00:00.000Z,
|
||||
2025-05-30T00:00:00.000Z,
|
||||
2026-05-30T00:00:00.000Z,
|
||||
2027-05-30T00:00:00.000Z,
|
||||
2028-05-30T00:00:00.000Z,
|
||||
2029-05-30T00:00:00.000Z,
|
||||
2030-05-30T00:00:00.000Z,
|
||||
2031-05-30T00:00:00.000Z,
|
||||
2032-05-30T00:00:00.000Z,
|
||||
2033-05-30T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('byhour, byminute, bysecond', () => {
|
||||
|
@ -844,6 +1067,30 @@ describe('RRule', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
it('ignores invalid byyearday values', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2020),
|
||||
freq: Frequency.YEARLY,
|
||||
byyearday: [0, -1],
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
});
|
||||
|
||||
expect(rule.all(10)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2020-01-01T00:00:00.000Z,
|
||||
2021-01-01T00:00:00.000Z,
|
||||
2022-01-01T00:00:00.000Z,
|
||||
2023-01-01T00:00:00.000Z,
|
||||
2024-01-01T00:00:00.000Z,
|
||||
2025-01-01T00:00:00.000Z,
|
||||
2026-01-01T00:00:00.000Z,
|
||||
2027-01-01T00:00:00.000Z,
|
||||
2028-01-01T00:00:00.000Z,
|
||||
2029-01-01T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
|
@ -872,5 +1119,33 @@ describe('RRule', () => {
|
|||
`"Cannot create RRule: until is an invalid date"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error on an interval of 0', () => {
|
||||
const testFn = () =>
|
||||
new RRule({
|
||||
dtstart: new Date(DATE_2020),
|
||||
freq: Frequency.HOURLY,
|
||||
interval: 0,
|
||||
tzid: 'UTC',
|
||||
});
|
||||
expect(testFn).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Cannot create RRule: interval must be greater than 0"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when exceeding the iteration limit', () => {
|
||||
const testFn = () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2020),
|
||||
freq: Frequency.YEARLY,
|
||||
byyearday: [1],
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
});
|
||||
rule.all(100001);
|
||||
};
|
||||
|
||||
expect(testFn).toThrowErrorMatchingInlineSnapshot(`"RRule iteration limit exceeded"`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,57 +7,16 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import moment, { Moment } from 'moment-timezone';
|
||||
import moment, { type Moment } from 'moment-timezone';
|
||||
|
||||
export enum Frequency {
|
||||
YEARLY = 0,
|
||||
MONTHLY = 1,
|
||||
WEEKLY = 2,
|
||||
DAILY = 3,
|
||||
HOURLY = 4,
|
||||
MINUTELY = 5,
|
||||
}
|
||||
|
||||
export enum Weekday {
|
||||
MO = 1,
|
||||
TU = 2,
|
||||
WE = 3,
|
||||
TH = 4,
|
||||
FR = 5,
|
||||
SA = 6,
|
||||
SU = 7,
|
||||
}
|
||||
|
||||
export type WeekdayStr = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
|
||||
interface IterOptions {
|
||||
refDT: Moment;
|
||||
wkst?: Weekday | number | null;
|
||||
byyearday?: number[] | null;
|
||||
bymonth?: number[] | null;
|
||||
bysetpos?: number[] | null;
|
||||
bymonthday?: number[] | null;
|
||||
byweekday?: Weekday[] | null;
|
||||
byhour?: number[] | null;
|
||||
byminute?: number[] | null;
|
||||
bysecond?: number[] | null;
|
||||
}
|
||||
|
||||
type Options = Omit<IterOptions, 'refDT'> & {
|
||||
dtstart: Date;
|
||||
freq?: Frequency;
|
||||
interval?: number;
|
||||
until?: Date | null;
|
||||
count?: number;
|
||||
tzid: string;
|
||||
};
|
||||
import { Frequency, Weekday, type WeekdayStr, type Options, type IterOptions } from './types';
|
||||
import { sanitizeOptions } from './sanitize';
|
||||
|
||||
type ConstructorOptions = Omit<Options, 'byweekday' | 'wkst'> & {
|
||||
byweekday?: Array<string | number> | null;
|
||||
wkst?: Weekday | WeekdayStr | number | null;
|
||||
};
|
||||
|
||||
export type { ConstructorOptions as Options };
|
||||
|
||||
const ISO_WEEKDAYS = [
|
||||
Weekday.MO,
|
||||
Weekday.TU,
|
||||
|
@ -73,19 +32,15 @@ type AllResult = Date[] & {
|
|||
};
|
||||
|
||||
const ALL_LIMIT = 10000;
|
||||
const TIMEOUT_LIMIT = 100000;
|
||||
|
||||
export class RRule {
|
||||
private options: Options;
|
||||
constructor(options: ConstructorOptions) {
|
||||
this.options = options as Options;
|
||||
if (isNaN(options.dtstart.getTime())) {
|
||||
throw new Error('Cannot create RRule: dtstart is an invalid date');
|
||||
}
|
||||
if (options.until && isNaN(options.until.getTime())) {
|
||||
throw new Error('Cannot create RRule: until is an invalid date');
|
||||
}
|
||||
this.options = sanitizeOptions(options as Options);
|
||||
if (typeof options.wkst === 'string') {
|
||||
this.options.wkst = Weekday[options.wkst];
|
||||
if (!this.options.wkst) delete this.options.wkst;
|
||||
}
|
||||
const weekdayParseResult = parseByWeekdayPos(options.byweekday);
|
||||
if (weekdayParseResult) {
|
||||
|
@ -111,12 +66,17 @@ export class RRule {
|
|||
.toDate();
|
||||
|
||||
const nextRecurrences: Moment[] = [];
|
||||
let iters = 0;
|
||||
|
||||
while (
|
||||
(!count && !until) ||
|
||||
(count && yieldedRecurrenceCount < count) ||
|
||||
(until && current.getTime() < new Date(until).getTime())
|
||||
(until && current.getTime() < until.getTime())
|
||||
) {
|
||||
iters++;
|
||||
if (iters > TIMEOUT_LIMIT) {
|
||||
throw new Error('RRule iteration limit exceeded');
|
||||
}
|
||||
const next = nextRecurrences.shift()?.toDate();
|
||||
if (next) {
|
||||
current = next;
|
||||
|
@ -305,6 +265,7 @@ const getYearOfRecurrences = function ({
|
|||
return getMonthOfRecurrences({
|
||||
refDT: currentMonth,
|
||||
wkst,
|
||||
bymonth,
|
||||
bymonthday,
|
||||
byweekday,
|
||||
byhour,
|
||||
|
@ -319,6 +280,7 @@ const getYearOfRecurrences = function ({
|
|||
|
||||
return derivedByyearday.flatMap((dayOfYear) => {
|
||||
const currentDate = moment(refDT).dayOfYear(dayOfYear);
|
||||
if (currentDate.year() !== refDT.year()) return [];
|
||||
if (!derivedByweekday.includes(currentDate.isoWeekday())) return [];
|
||||
return getDayOfRecurrences({ refDT: currentDate, byhour, byminute, bysecond });
|
||||
});
|
||||
|
@ -337,7 +299,7 @@ const getMonthOfRecurrences = function ({
|
|||
}: IterOptions) {
|
||||
const derivedByweekday = byweekday ?? ISO_WEEKDAYS;
|
||||
const currentMonth = refDT.month();
|
||||
if (bymonth && !bymonth.includes(currentMonth)) return [];
|
||||
if (bymonth && !bymonth.includes(currentMonth + 1)) return [];
|
||||
|
||||
let derivedBymonthday = bymonthday ?? [refDT.date()];
|
||||
if (bysetpos) {
|
||||
|
@ -384,6 +346,7 @@ const getMonthOfRecurrences = function ({
|
|||
|
||||
return derivedBymonthday.flatMap((date) => {
|
||||
const currentDate = moment(refDT).date(date);
|
||||
if (bymonth && !bymonth.includes(currentDate.month() + 1)) return [];
|
||||
if (!derivedByweekday.includes(currentDate.isoWeekday())) return [];
|
||||
return getDayOfRecurrences({ refDT: currentDate, byhour, byminute, bysecond });
|
||||
});
|
||||
|
|
131
packages/kbn-rrule/sanitize.test.ts
Normal file
131
packages/kbn-rrule/sanitize.test.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { sanitizeOptions } from './sanitize';
|
||||
import { Weekday, Frequency, type Options } from './types';
|
||||
|
||||
describe('sanitizeOptions', () => {
|
||||
const options: Options = {
|
||||
wkst: Weekday.MO,
|
||||
byyearday: [1, 2, 3],
|
||||
bymonth: [1],
|
||||
bysetpos: [1],
|
||||
bymonthday: [1],
|
||||
byweekday: [Weekday.MO],
|
||||
byhour: [1],
|
||||
byminute: [1],
|
||||
bysecond: [1],
|
||||
dtstart: new Date('September 3, 1998 03:24:00'),
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
until: new Date('February 25, 2022 03:24:00'),
|
||||
count: 3,
|
||||
tzid: 'foobar',
|
||||
};
|
||||
|
||||
it('happy path', () => {
|
||||
expect(sanitizeOptions(options)).toEqual(options);
|
||||
});
|
||||
|
||||
it('throws an error when dtstart is missing', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => sanitizeOptions({ ...options, dtstart: null })).toThrowError(
|
||||
'Cannot create RRule: dtstart is required'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when tzid is missing', () => {
|
||||
expect(() => sanitizeOptions({ ...options, tzid: '' })).toThrowError(
|
||||
'Cannot create RRule: tzid is required'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when until field is invalid', () => {
|
||||
expect(() =>
|
||||
sanitizeOptions({
|
||||
...options,
|
||||
// @ts-expect-error
|
||||
until: {
|
||||
getTime: () => NaN,
|
||||
},
|
||||
})
|
||||
).toThrowError('Cannot create RRule: until is an invalid date');
|
||||
});
|
||||
|
||||
it('throws an error when interval is less than 0', () => {
|
||||
expect(() => sanitizeOptions({ ...options, interval: -3 })).toThrowError(
|
||||
'Cannot create RRule: interval must be greater than 0'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when interval is not a number', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => sanitizeOptions({ ...options, interval: 'foobar' })).toThrowError(
|
||||
'Cannot create RRule: interval must be a number'
|
||||
);
|
||||
});
|
||||
|
||||
it('filters out invalid bymonth values', () => {
|
||||
expect(sanitizeOptions({ ...options, bymonth: [0, 6, 13] })).toEqual({
|
||||
...options,
|
||||
bymonth: [6],
|
||||
});
|
||||
});
|
||||
|
||||
it('removes bymonth when it is empty', () => {
|
||||
expect(sanitizeOptions({ ...options, bymonth: [0] })).toEqual({
|
||||
...options,
|
||||
bymonth: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('filters out invalid bymonthday values', () => {
|
||||
expect(sanitizeOptions({ ...options, bymonthday: [0, 15, 32] })).toEqual({
|
||||
...options,
|
||||
bymonthday: [15],
|
||||
});
|
||||
});
|
||||
|
||||
it('removes bymonthday when it is empty', () => {
|
||||
expect(sanitizeOptions({ ...options, bymonthday: [0] })).toEqual({
|
||||
...options,
|
||||
bymonthday: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('filters out invalid byweekday values', () => {
|
||||
// @ts-expect-error
|
||||
expect(sanitizeOptions({ ...options, byweekday: [0, 4, 8] })).toEqual({
|
||||
...options,
|
||||
byweekday: [4],
|
||||
});
|
||||
});
|
||||
|
||||
it('removes byweekday when it is empty', () => {
|
||||
// @ts-expect-error
|
||||
expect(sanitizeOptions({ ...options, byweekday: [0] })).toEqual({
|
||||
...options,
|
||||
byweekday: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('filters out invalid byyearday values', () => {
|
||||
expect(sanitizeOptions({ ...options, byyearday: [0, 150, 367] })).toEqual({
|
||||
...options,
|
||||
byyearday: [150],
|
||||
});
|
||||
});
|
||||
|
||||
it('removes byyearday when it is empty', () => {
|
||||
expect(sanitizeOptions({ ...options, byyearday: [0] })).toEqual({
|
||||
...options,
|
||||
byyearday: undefined,
|
||||
});
|
||||
});
|
||||
});
|
82
packages/kbn-rrule/sanitize.ts
Normal file
82
packages/kbn-rrule/sanitize.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { Options } from './types';
|
||||
|
||||
export function sanitizeOptions(opts: Options) {
|
||||
const options = { ...opts };
|
||||
|
||||
// Guard against invalid options that can't be omitted
|
||||
if (!options.dtstart) {
|
||||
throw new Error('Cannot create RRule: dtstart is required');
|
||||
}
|
||||
|
||||
if (!options.tzid) {
|
||||
throw new Error('Cannot create RRule: tzid is required');
|
||||
}
|
||||
|
||||
if (isNaN(options.dtstart.getTime())) {
|
||||
throw new Error('Cannot create RRule: dtstart is an invalid date');
|
||||
}
|
||||
|
||||
if (options.until && isNaN(options.until.getTime())) {
|
||||
throw new Error('Cannot create RRule: until is an invalid date');
|
||||
}
|
||||
|
||||
if (options.interval != null) {
|
||||
if (typeof options.interval !== 'number') {
|
||||
throw new Error('Cannot create RRule: interval must be a number');
|
||||
}
|
||||
|
||||
if (options.interval < 1) {
|
||||
throw new Error('Cannot create RRule: interval must be greater than 0');
|
||||
}
|
||||
}
|
||||
|
||||
// Omit invalid options
|
||||
if (options.bymonth) {
|
||||
// Only months between 1 and 12 are valid
|
||||
options.bymonth = options.bymonth.filter(
|
||||
(month) => typeof month === 'number' && month >= 1 && month <= 12
|
||||
);
|
||||
if (!options.bymonth.length) {
|
||||
delete options.bymonth;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.bymonthday) {
|
||||
// Only days between 1 and 31 are valid
|
||||
options.bymonthday = options.bymonthday.filter(
|
||||
(day) => typeof day === 'number' && day >= 1 && day <= 31
|
||||
);
|
||||
if (!options.bymonthday.length) {
|
||||
delete options.bymonthday;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.byweekday) {
|
||||
// Only weekdays between 1 and 7 are valid
|
||||
options.byweekday = options.byweekday.filter(
|
||||
(weekday) => typeof weekday === 'number' && weekday >= 1 && weekday <= 7
|
||||
);
|
||||
if (!options.byweekday.length) {
|
||||
delete options.byweekday;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.byyearday) {
|
||||
// Only days between 1 and 366 are valid
|
||||
options.byyearday = options.byyearday.filter((day) => day >= 1 && day <= 366);
|
||||
if (!options.byyearday.length) {
|
||||
delete options.byyearday;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
53
packages/kbn-rrule/types.ts
Normal file
53
packages/kbn-rrule/types.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { Moment } from 'moment';
|
||||
|
||||
export enum Frequency {
|
||||
YEARLY = 0,
|
||||
MONTHLY = 1,
|
||||
WEEKLY = 2,
|
||||
DAILY = 3,
|
||||
HOURLY = 4,
|
||||
MINUTELY = 5,
|
||||
}
|
||||
|
||||
export enum Weekday {
|
||||
MO = 1,
|
||||
TU = 2,
|
||||
WE = 3,
|
||||
TH = 4,
|
||||
FR = 5,
|
||||
SA = 6,
|
||||
SU = 7,
|
||||
}
|
||||
|
||||
export type WeekdayStr = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
|
||||
|
||||
export interface IterOptions {
|
||||
refDT: Moment;
|
||||
wkst?: Weekday | number | null;
|
||||
byyearday?: number[] | null;
|
||||
bymonth?: number[] | null;
|
||||
bysetpos?: number[] | null;
|
||||
bymonthday?: number[] | null;
|
||||
byweekday?: Weekday[] | null;
|
||||
byhour?: number[] | null;
|
||||
byminute?: number[] | null;
|
||||
bysecond?: number[] | null;
|
||||
}
|
||||
|
||||
export type Options = Omit<IterOptions, 'refDT'> & {
|
||||
dtstart: Date;
|
||||
freq?: Frequency;
|
||||
interval?: number;
|
||||
until?: Date | null;
|
||||
count?: number;
|
||||
tzid: string;
|
||||
};
|
|
@ -34,17 +34,28 @@ export const rRuleRequestSchema = schema.object({
|
|||
})
|
||||
),
|
||||
byweekday: schema.maybe(
|
||||
schema.arrayOf(schema.string(), {
|
||||
validate: createValidateRecurrenceByV1('byweekday'),
|
||||
})
|
||||
schema.arrayOf(
|
||||
schema.oneOf([
|
||||
schema.literal('MO'),
|
||||
schema.literal('TU'),
|
||||
schema.literal('WE'),
|
||||
schema.literal('TH'),
|
||||
schema.literal('FR'),
|
||||
schema.literal('SA'),
|
||||
schema.literal('SU'),
|
||||
]),
|
||||
{
|
||||
validate: createValidateRecurrenceByV1('byweekday'),
|
||||
}
|
||||
)
|
||||
),
|
||||
bymonthday: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
schema.arrayOf(schema.number({ min: 1, max: 31 }), {
|
||||
validate: createValidateRecurrenceByV1('bymonthday'),
|
||||
})
|
||||
),
|
||||
bymonth: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
schema.arrayOf(schema.number({ min: 1, max: 12 }), {
|
||||
validate: createValidateRecurrenceByV1('bymonth'),
|
||||
})
|
||||
),
|
||||
|
|
|
@ -29,17 +29,28 @@ export const rRuleRequestSchema = schema.object({
|
|||
})
|
||||
),
|
||||
byweekday: schema.maybe(
|
||||
schema.arrayOf(schema.string(), {
|
||||
validate: createValidateRecurrenceBy('byweekday'),
|
||||
})
|
||||
schema.arrayOf(
|
||||
schema.oneOf([
|
||||
schema.literal('MO'),
|
||||
schema.literal('TU'),
|
||||
schema.literal('WE'),
|
||||
schema.literal('TH'),
|
||||
schema.literal('FR'),
|
||||
schema.literal('SA'),
|
||||
schema.literal('SU'),
|
||||
]),
|
||||
{
|
||||
validate: createValidateRecurrenceBy('byweekday'),
|
||||
}
|
||||
)
|
||||
),
|
||||
bymonthday: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
schema.arrayOf(schema.number({ min: 1, max: 31 }), {
|
||||
validate: createValidateRecurrenceBy('bymonthday'),
|
||||
})
|
||||
),
|
||||
bymonth: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
schema.arrayOf(schema.number({ min: 1, max: 12 }), {
|
||||
validate: createValidateRecurrenceBy('bymonth'),
|
||||
})
|
||||
),
|
||||
|
|
|
@ -230,4 +230,25 @@ describe('isSnoozeActive', () => {
|
|||
expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
test('snooze still works with invalid bymonth value', () => {
|
||||
// Set the current time as:
|
||||
// - Feb 27 2023 08:15:00 GMT+0000 - Monday
|
||||
fakeTimer = sinon.useFakeTimers(new Date('2023-02-09T08:15:00.000Z'));
|
||||
|
||||
const snoozeA = {
|
||||
duration: moment('2023-01', 'YYYY-MM').daysInMonth() * 24 * 60 * 60 * 1000, // 1 month
|
||||
rRule: {
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
bymonthday: [1],
|
||||
bymonth: [0],
|
||||
tzid: 'Europe/Madrid',
|
||||
dtstart: '2023-01-01T00:00:00.000Z',
|
||||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue