@wmfs/tymly-schedule-plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ version: 2
2
+ jobs:
3
+ unit test:
4
+ docker:
5
+ - image: cimg/node:lts
6
+ auth:
7
+ username: $DOCKERHUB_USERNAME
8
+ password: $DOCKERHUB_PASSWORD
9
+ environment:
10
+ TZ: "Europe/London"
11
+ PG_CONNECTION_STRING: postgres://postgres:postgres@localhost:5432/circle_test
12
+ TIMEOUT: 15000
13
+ - image: cimg/postgres:13.4-postgis
14
+ auth:
15
+ username: $DOCKERHUB_USERNAME
16
+ password: $DOCKERHUB_PASSWORD
17
+ environment:
18
+ POSTGRES_USER: postgres
19
+ POSTGRES_HOST_AUTH_METHOD: trust
20
+ POSTGRES_DB: circle_test
21
+ TZ: "Europe/London"
22
+ working_directory: ~/repo
23
+ steps:
24
+ - checkout
25
+ - restore_cache:
26
+ keys:
27
+ - v1-deps-{{ checksum "package.json" }}
28
+ - v1-deps-
29
+ - run:
30
+ name: install
31
+ command: |
32
+ npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN
33
+ npm install
34
+ - save_cache:
35
+ key: v1-deps-{{ checksum "package.json" }}
36
+ paths:
37
+ - node_modules
38
+ - run:
39
+ name: test
40
+ command: npm test
41
+ - run:
42
+ name: codecov
43
+ command: npm run coverage
44
+ lint:
45
+ docker:
46
+ - image: cimg/node:lts
47
+ auth:
48
+ username: $DOCKERHUB_USERNAME
49
+ password: $DOCKERHUB_PASSWORD
50
+ working_directory: ~/repo
51
+ steps:
52
+ - checkout
53
+ - run:
54
+ name: install standard
55
+ command: |
56
+ npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN
57
+ npm install
58
+ - run:
59
+ name: lint
60
+ command: npm run lint
61
+ release:
62
+ docker:
63
+ - image: cimg/node:lts
64
+ auth:
65
+ username: $DOCKERHUB_USERNAME
66
+ password: $DOCKERHUB_PASSWORD
67
+ environment:
68
+ TZ: "Europe/London"
69
+ working_directory: ~/repo
70
+ steps:
71
+ - checkout
72
+ - restore_cache:
73
+ keys:
74
+ - v1-deps-{{ checksum "package.json" }}
75
+ - v1-deps-
76
+ - run:
77
+ name: install
78
+ command: |
79
+ npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN
80
+ npm install
81
+ - save_cache:
82
+ key: v1-deps-{{ checksum "package.json" }}
83
+ paths:
84
+ - node_modules
85
+ - run:
86
+ name: release
87
+ command: |
88
+ chmod +x version_plugin
89
+ npx semantic-release
90
+ workflows:
91
+ version: 2
92
+ test_and_release:
93
+ jobs:
94
+ - unit test:
95
+ context:
96
+ - docker-hub-creds
97
+ - build-env-vars
98
+ - lint:
99
+ context:
100
+ - docker-hub-creds
101
+ - build-env-vars
102
+ - release:
103
+ context:
104
+ - docker-hub-creds
105
+ - build-env-vars
106
+ requires:
107
+ - unit test
108
+ - lint
109
+ filters:
110
+ branches:
111
+ only: master
@@ -0,0 +1,38 @@
1
+ {
2
+ "branch": "master",
3
+ "analyzeCommits": {
4
+ "preset": "angular",
5
+ "releaseRules": [
6
+ {"type": "build", "scope":"deps", "release": "minor"}
7
+ ]
8
+ },
9
+ "verifyConditions": [
10
+ "@semantic-release/changelog",
11
+ "@semantic-release/npm",
12
+ "@semantic-release/git"
13
+ ],
14
+ "generateNotes": {
15
+ "preset": "metahub"
16
+ },
17
+ "prepare": [
18
+ {
19
+ "path": "@semantic-release/exec",
20
+ "cmd": "echo 'VERSION=${nextRelease.version}' > .version && ./version_plugin"
21
+ },
22
+ {
23
+ "path": "@semantic-release/changelog",
24
+ "changelogFile": "CHANGELOG.md"
25
+ },
26
+ {
27
+ "path": "@semantic-release/git",
28
+ "assets": ["CHANGELOG.md"]
29
+ },
30
+ "@semantic-release/npm"
31
+ ],
32
+ "publish": [
33
+ "@semantic-release/npm",
34
+ "@semantic-release/github"
35
+ ],
36
+ "success": ["@semantic-release/github"],
37
+ "fail": ["@semantic-release/github"]
38
+ }
package/.version ADDED
@@ -0,0 +1 @@
1
+ VERSION=1.0.0
package/CHANGELOG.md ADDED
@@ -0,0 +1,41 @@
1
+ # 1.0.0 (2022-03-15)
2
+
3
+
4
+ ### ✨ Features
5
+
6
+ * ability to stop tasks ([1540f7a](https://github.com/wmfs/tymly-schedule-plugin/commit/1540f7ae85aac4ae9cdd87b3b12ee9ddadf6beb3))
7
+ * allow for timezone option ([6dc4082](https://github.com/wmfs/tymly-schedule-plugin/commit/6dc408296538b4f3815a5aad372b140ce5651529))
8
+ * allow rescheduling ([3b3f1f9](https://github.com/wmfs/tymly-schedule-plugin/commit/3b3f1f9e7e4b788a24ee45559e351a873d7c46dc))
9
+ * allow rescheduling, cron style or date style ([a55b75c](https://github.com/wmfs/tymly-schedule-plugin/commit/a55b75c5a639ad5aa49cbaea929fbc9ff82dea09))
10
+ * function to convert datetime back to rule ([b7b68e7](https://github.com/wmfs/tymly-schedule-plugin/commit/b7b68e7c4836001965243fef22773703553f574e))
11
+ * move rule/datetime converting into utils directory ([a425105](https://github.com/wmfs/tymly-schedule-plugin/commit/a42510596a87bb1fc617a18526bb9b1d14b92ab3))
12
+
13
+
14
+ ### 🐛 Bug Fixes
15
+
16
+ * add log for execution error ([c49d261](https://github.com/wmfs/tymly-schedule-plugin/commit/c49d26104a72be787d361b1df2381ecbe5ca13f2))
17
+ * issues with scheduling for dates ([9c4b40f](https://github.com/wmfs/tymly-schedule-plugin/commit/9c4b40f9bf88ae7eaf8398a9634c065f935d91ae))
18
+ * persist schedule config from db ([5034eff](https://github.com/wmfs/tymly-schedule-plugin/commit/5034eff6dd953653b1094f132112992b25232413))
19
+ * rename some things ([b42cf44](https://github.com/wmfs/tymly-schedule-plugin/commit/b42cf447eb02e20435498c19c159fb64f787293e))
20
+
21
+
22
+ ### 📦 Code Refactoring
23
+
24
+ * use node-schedule instead ([a8e57ff](https://github.com/wmfs/tymly-schedule-plugin/commit/a8e57ffe1a7d4728596eac187cfe7229f6a4d6a3))
25
+
26
+
27
+ ### 🚨 Tests
28
+
29
+ * more tests ([87f9de1](https://github.com/wmfs/tymly-schedule-plugin/commit/87f9de1a1133d0ffe1421e699d260da3f677af85))
30
+
31
+
32
+ ### ⚙️ Continuous Integrations
33
+
34
+ * add db config for circle ([4541e9d](https://github.com/wmfs/tymly-schedule-plugin/commit/4541e9d9df5794c55acc618ff8ce3fa5aa03a4ef))
35
+
36
+
37
+ ### ♻️ Chores
38
+
39
+ * disable tymly sync ([d0776ba](https://github.com/wmfs/tymly-schedule-plugin/commit/d0776ba009ae234a5644b64911142ef8abc7bc54))
40
+ * initial commit, simple state machine scheduling ([26d7163](https://github.com/wmfs/tymly-schedule-plugin/commit/26d7163980df98100bf561237af339123c710e8d))
41
+ * rename to tymly schedule plugin ([2937228](https://github.com/wmfs/tymly-schedule-plugin/commit/29372288cfeefba537afdb6d65e4c11709ddfbb0))
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 West Midlands Fire Service
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # tymly-schedule-plugin
2
+ [![Tymly Package](https://img.shields.io/badge/tymly-package-blue.svg)](https://tymly.io/)
3
+ [![npm (scoped)](https://img.shields.io/npm/v/@wmfs/tymly-schedule-plugin.svg)](https://www.npmjs.com/package/@wmfs/tymly-schedule-plugin)
4
+ [![CircleCI](https://circleci.com/gh/wmfs/tymly-schedule-plugin.svg?style=svg)](https://circleci.com/gh/wmfs/tymly-schedule-plugin)[![codecov](https://codecov.io/gh/wmfs/tymly-schedule-plugin/branch/master/graph/badge.svg)](https://codecov.io/gh/wmfs/tymly-schedule-plugin)
5
+ [![CodeFactor](https://www.codefactor.io/repository/github/wmfs/tymly-schedule-plugin/badge)](https://www.codefactor.io/repository/github/wmfs/tymly-schedule-plugin)
6
+ [![Dependabot badge](https://img.shields.io/badge/Dependabot-active-brightgreen.svg)](https://dependabot.com/)
7
+ [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
8
+ [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
9
+ [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/wmfs/tymly-schedule-plugin/blob/master/LICENSE)
10
+
11
+ > Provides state machine scheduling for the Tymly framework
12
+
13
+ ## <a name="install"></a>Install
14
+ ```bash
15
+ $ npm install tymly-schedule-plugin --save
16
+ ```
17
+
18
+
19
+ ## <a name="license"></a>License
20
+
21
+ MIT
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "schedule",
3
+ "namespace": "schedule",
4
+ "version": "2.171.0",
5
+ "label": "Tymly schedule plugin for scheduling state machines",
6
+ "author": "WMFS",
7
+ "description": "Tymly schedule plugin for scheduling state machines",
8
+ "tags": []
9
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "title": "State machine schedules",
3
+ "description": "Stores schedule configuration for state machines",
4
+ "type": "object",
5
+ "primaryKey": [
6
+ "key"
7
+ ],
8
+ "properties": {
9
+ "key": {
10
+ "type": "string"
11
+ },
12
+ "stateMachineName": {
13
+ "type": "string"
14
+ },
15
+ "input": {
16
+ "type": "object"
17
+ },
18
+ "sendResponse": {
19
+ "type": "string"
20
+ },
21
+ "scheduleType": {
22
+ "type": "string",
23
+ "enum": [
24
+ "interval",
25
+ "datetime"
26
+ ]
27
+ },
28
+ "interval": {
29
+ "type": "string"
30
+ },
31
+ "datetime": {
32
+ "type": "string",
33
+ "format": "date-time"
34
+ },
35
+ "startOnBoot": {
36
+ "type": "boolean"
37
+ },
38
+ "lastRanStart": {
39
+ "type": "string",
40
+ "format": "date-time"
41
+ },
42
+ "lastRanEnd": {
43
+ "type": "string",
44
+ "format": "date-time"
45
+ },
46
+ "lastRanExecutionName": {
47
+ "type": "string"
48
+ },
49
+ "totalRunCount": {
50
+ "type": "integer"
51
+ },
52
+ "status": {
53
+ "type": "string"
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ module.exports = {
4
+ description: 'Tymly Schedule Service'
5
+ }
@@ -0,0 +1,225 @@
1
+ const nodeSchedule = require('node-schedule')
2
+ const ruleToDatetime = require('./utils/rule-to-datetime')
3
+ const datetimeToRule = require('./utils/datetime-to-rule')
4
+
5
+ class CronService {
6
+ async boot (options) {
7
+ this.statebox = options.bootedServices.statebox
8
+ this.taskModel = options.bootedServices.storage.models.schedule_task
9
+ this.scheduledTasks = await this.findTasks(options.blueprintComponents.stateMachineSchedules)
10
+
11
+ for (const [key, task] of Object.entries(this.scheduledTasks)) {
12
+ if (task.startOnBoot === true) {
13
+ await this.startTask(key)
14
+ }
15
+ }
16
+ } // boot
17
+
18
+ startTask (key) {
19
+ const task = this.scheduledTasks[key]
20
+
21
+ if (!task) {
22
+ console.log(`Cannot find schedule task to start '${key}'`)
23
+ return
24
+ }
25
+
26
+ const {
27
+ scheduleType,
28
+ interval,
29
+ datetime,
30
+ stateMachineName,
31
+ sendResponse,
32
+ input
33
+ } = task
34
+
35
+ const jobOptions = {
36
+ tz: 'UTC'
37
+ }
38
+
39
+ if (scheduleType === 'interval') {
40
+ jobOptions.rule = interval
41
+ } else if (scheduleType === 'datetime') {
42
+ const rule = datetimeToRule(datetime)
43
+
44
+ Object
45
+ .entries(rule)
46
+ .forEach(([key, value]) => {
47
+ jobOptions[key] = value
48
+ })
49
+ }
50
+
51
+ // todo: allow window for start/end datetimes
52
+
53
+ task.job = nodeSchedule.scheduleJob(
54
+ jobOptions,
55
+ () => {
56
+ console.log(`Running scheduled state machine ${stateMachineName}`)
57
+ const startedAt = new Date()
58
+ const executionOptions = {
59
+ instigatingClient: {
60
+ appName: 'tymly',
61
+ domain: 'tymly-schedule-plugin'
62
+ },
63
+ sendResponse
64
+ }
65
+ const cb = async (err, executionDescription) => {
66
+ if (err) {
67
+ console.log(`There was an error on execution of scheduled state machine ${stateMachineName}`)
68
+ }
69
+ await this.updateTaskOnExecution(key, executionDescription.executionName, startedAt)
70
+ }
71
+ this.statebox.startExecution(input, stateMachineName, executionOptions, cb)
72
+ }
73
+ )
74
+
75
+ this.scheduledTasks[key] = task
76
+
77
+ return this.updateTaskStatus(key, 'STARTED')
78
+ } // startTask
79
+
80
+ stopTask (key) {
81
+ const task = this.scheduledTasks[key]
82
+
83
+ if (!task) {
84
+ console.log(`Cannot find schedule task to stop '${key}'`)
85
+ return
86
+ }
87
+
88
+ if (!task.job) {
89
+ console.log(`Cannot find job on task to stop '${key}'`)
90
+ return
91
+ }
92
+
93
+ task.job.cancel()
94
+
95
+ return this.updateTaskStatus(key, 'STOPPED')
96
+ } // stopTask
97
+
98
+ updateTaskStatus (key, status) {
99
+ return this.taskModel.update(
100
+ {
101
+ key,
102
+ status
103
+ },
104
+ {
105
+ setMissingPropertiesToNull: false
106
+ }
107
+ )
108
+ }
109
+
110
+ stopTasks () {
111
+ return Promise.all(
112
+ Object.keys(this.scheduledTasks).map(key => this.stopTask(key))
113
+ )
114
+ } // stopTasks
115
+
116
+ async findTasks (blueprintTasks) {
117
+ const tasks = {}
118
+
119
+ // From database
120
+ const persistedTasks = await this.taskModel.find({})
121
+
122
+ for (const config of persistedTasks) {
123
+ tasks[config.key] = config
124
+ }
125
+
126
+ // From blueprint
127
+ for (const [key, config] of Object.entries(blueprintTasks)) {
128
+ if (tasks[key]) {
129
+ continue
130
+ }
131
+
132
+ // todo: recurrence rules?
133
+
134
+ if (typeof config.interval === 'string') {
135
+ config.scheduleType = 'interval'
136
+ config.datetime = null
137
+ } else if (typeof config.rule === 'object') {
138
+ config.scheduleType = 'datetime'
139
+ config.datetime = ruleToDatetime(config.rule)
140
+ config.interval = null
141
+ }
142
+
143
+ tasks[key] = config
144
+
145
+ await this.persistTask(key, config)
146
+ }
147
+
148
+ return tasks
149
+ } // findTasks
150
+
151
+ persistTask (key, config) {
152
+ return this.taskModel.create({
153
+ key,
154
+ stateMachineName: config.stateMachineName,
155
+ input: config.input,
156
+ sendResponse: config.sendResponse,
157
+ scheduleType: config.scheduleType,
158
+ interval: config.interval,
159
+ datetime: config.datetime,
160
+ startOnBoot: config.startOnBoot,
161
+ lastRanStart: null,
162
+ lastRanEnd: null,
163
+ lastRanExecutionName: null,
164
+ totalRunCount: 0,
165
+ status: null
166
+ })
167
+ }
168
+
169
+ async updateTaskOnExecution (key, executionName, startedAt) {
170
+ const { totalRunCount } = await this.taskModel.findOne(
171
+ {
172
+ where: { key: { equals: key } },
173
+ fields: ['totalRunCount']
174
+ }
175
+ )
176
+
177
+ return this.taskModel.update(
178
+ {
179
+ key,
180
+ lastRanStart: startedAt,
181
+ lastRanEnd: new Date(),
182
+ lastRanExecutionName: executionName,
183
+ totalRunCount: totalRunCount + 1
184
+ },
185
+ {
186
+ setMissingPropertiesToNull: false
187
+ }
188
+ )
189
+ }
190
+
191
+ async stopAndUpdateTaskSchedule (key, rule) {
192
+ await this.stopTask(key)
193
+
194
+ if (typeof rule === 'string') {
195
+ this.scheduledTasks[key].interval = rule
196
+ this.scheduledTasks[key].datetime = null
197
+ this.scheduledTasks[key].scheduleType = 'interval'
198
+ } else if (typeof rule === 'object') {
199
+ this.scheduledTasks[key].interval = null
200
+ this.scheduledTasks[key].datetime = ruleToDatetime(rule)
201
+ this.scheduledTasks[key].scheduleType = 'datetime'
202
+ }
203
+
204
+ return this.taskModel.update(
205
+ {
206
+ key,
207
+ interval: this.scheduledTasks[key].interval,
208
+ datetime: this.scheduledTasks[key].datetime,
209
+ scheduleType: this.scheduledTasks[key].scheduleType
210
+ },
211
+ {
212
+ setMissingPropertiesToNull: false
213
+ }
214
+ )
215
+ }
216
+
217
+ async shutdown () {
218
+ await nodeSchedule.gracefulShutdown()
219
+ } // shutdown
220
+ } // class CronService
221
+
222
+ module.exports = {
223
+ serviceClass: CronService,
224
+ bootAfter: ['statebox']
225
+ }
@@ -0,0 +1,14 @@
1
+ module.exports = function (datetime) {
2
+ if (!(datetime instanceof Date)) {
3
+ datetime = new Date()
4
+ }
5
+
6
+ return {
7
+ year: datetime.getFullYear(),
8
+ month: datetime.getMonth(),
9
+ date: datetime.getDate(),
10
+ hour: datetime.getHours(),
11
+ minute: datetime.getMinutes(),
12
+ second: datetime.getSeconds()
13
+ }
14
+ }
@@ -0,0 +1,11 @@
1
+ module.exports = function (rule = {}) {
2
+ const now = new Date()
3
+ return new Date(
4
+ isNaN(rule.year) ? now.getFullYear() : rule.year,
5
+ isNaN(rule.month) ? now.getMonth() : rule.month,
6
+ isNaN(rule.date) ? now.getDate() : rule.date,
7
+ isNaN(rule.hour) ? now.getHours() : rule.hour,
8
+ isNaN(rule.minute) ? now.getMinutes() : rule.minute,
9
+ isNaN(rule.second) ? now.getSeconds() : rule.second
10
+ )
11
+ }
package/lib/index.js ADDED
@@ -0,0 +1,2 @@
1
+ 'use strict'
2
+ module.exports = {}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@wmfs/tymly-schedule-plugin",
3
+ "version": "1.0.0",
4
+ "description": "Provides state machine scheduling for the Tymly framework",
5
+ "homepage": "https://github.com/wmfs/tymly-schedule-plugin#readme",
6
+ "author": "West Midlands Fire Service",
7
+ "keywords": [
8
+ "tymly",
9
+ "plugin",
10
+ "task",
11
+ "scheduling",
12
+ "schedule",
13
+ "cron"
14
+ ],
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/wmfs/tymly-schedule-plugin.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/wmfs/tymly-schedule-plugin/issues"
22
+ },
23
+ "main": "./lib/index.js",
24
+ "dependencies": {
25
+ "node-schedule": "2.1.0"
26
+ },
27
+ "devDependencies": {
28
+ "@semantic-release/changelog": "6.0.1",
29
+ "@semantic-release/exec": "6.0.3",
30
+ "@semantic-release/git": "10.0.1",
31
+ "@wmfs/tymly": "1.235.1",
32
+ "@wmfs/tymly-pg-plugin": "1.268.0",
33
+ "chai": "4.3.6",
34
+ "codecov": "3.8.3",
35
+ "conventional-changelog-metahub": "4.0.1",
36
+ "cz-conventional-changelog": "3.3.0",
37
+ "dirty-chai": "2.0.1",
38
+ "mocha": "9.2.2",
39
+ "moment": "2.29.1",
40
+ "nyc": "15.1.0",
41
+ "semantic-release": "19.0.2",
42
+ "standard": "16.0.4"
43
+ },
44
+ "scripts": {
45
+ "lint": "standard",
46
+ "test": "nyc mocha",
47
+ "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
48
+ "semantic-release": "semantic-release"
49
+ },
50
+ "config": {
51
+ "commitizen": {
52
+ "path": "cz-conventional-changelog"
53
+ },
54
+ "tymly": {
55
+ "sync": false
56
+ }
57
+ },
58
+ "publishConfig": {
59
+ "access": "restricted"
60
+ }
61
+ }
package/plugin.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "schedule-plugin",
3
+ "version": "1.0.0",
4
+ "label": "Tymly Schedule Plugin",
5
+ "author": "WMFS",
6
+ "organisation": "West Midlands Fire Service",
7
+ "description": "Provides state machine scheduling for the Tymly framework",
8
+ "categories": [
9
+ "cron",
10
+ "scheduling",
11
+ "schedule"
12
+ ]
13
+ }
package/renovate.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "extends": [
3
+ "config:base"
4
+ ],
5
+ "dependencyDashboard": true,
6
+ "enabledManagers": ["npm"],
7
+ "packageRules": [
8
+ {
9
+ "matchDepTypes": ["devDependencies"],
10
+ "automerge": true,
11
+ "semanticCommitType": "build",
12
+ "semanticCommitScope": "deps-dev",
13
+ "labels": ["devDependencies"]
14
+ },
15
+ {
16
+ "matchDepTypes": ["dependencies"],
17
+ "semanticCommitType": "build",
18
+ "semanticCommitScope": "deps",
19
+ "labels": ["dependencies"],
20
+ "reviewers": ["team:tymly-reviewers"]
21
+ }
22
+ ],
23
+ "vulnerabilityAlerts": {
24
+ "labels": ["security"]
25
+ }
26
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+
3
+ "namespace": "tymlyTest",
4
+ "name": "cats",
5
+ "version": "1.0",
6
+
7
+ "label": "Cats",
8
+ "author": "WMFS",
9
+
10
+ "organisation": "Tymlys",
11
+ "description": "An example Tymly Blueprint to help model cat behaviour...",
12
+ "categories": ["cats", "pets"]
13
+
14
+ }
@@ -0,0 +1,7 @@
1
+ module.exports = function () {
2
+ return function () {
3
+ const min = 1
4
+ const max = 10
5
+ return Math.floor(Math.random() * (max - min + 1) + min)
6
+ }
7
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "properties": {
3
+ "happinessLevel": {
4
+ "type": "integer"
5
+ },
6
+ "hungerLevel": {
7
+ "type": "integer"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "startOnBoot": true,
3
+ "interval": "*/2 * * * * *",
4
+ "input": {},
5
+ "stateMachineName": "tymlyTest_sendCatUpdate",
6
+ "sendResponse": "COMPLETE"
7
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "Comment": "Send cat update",
3
+ "StartAt": "HappinessLevel",
4
+ "States": {
5
+ "HappinessLevel": {
6
+ "Type": "Task",
7
+ "Resource": "function:tymlyTest_randomNumber",
8
+ "ResultPath": "$.happinessLevel",
9
+ "Next": "HungerLevel"
10
+ },
11
+ "HungerLevel": {
12
+ "Type": "Task",
13
+ "Resource": "function:tymlyTest_randomNumber",
14
+ "ResultPath": "$.hungerLevel",
15
+ "Next": "Upserting"
16
+ },
17
+ "Upserting": {
18
+ "Type": "Task",
19
+ "InputPath": "$",
20
+ "Resource": "module:upserting",
21
+ "ResourceConfig": {
22
+ "modelId": "tymlyTest_catStats"
23
+ },
24
+ "End": true
25
+ }
26
+ },
27
+ "restrictions": [
28
+ {
29
+ "roleId": "$everyone",
30
+ "allows": [
31
+ "*"
32
+ ]
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "Comment": "Send cat update",
3
+ "StartAt": "TirednessLevel",
4
+ "States": {
5
+ "TirednessLevel": {
6
+ "Type": "Task",
7
+ "Resource": "function:tymlyTest_randomNumber",
8
+ "ResultPath": "$.tirednessLevel",
9
+ "Next": "Upserting"
10
+ },
11
+ "Upserting": {
12
+ "Type": "Task",
13
+ "InputPath": "$",
14
+ "Resource": "module:upserting",
15
+ "ResourceConfig": {
16
+ "modelId": "tymlyTest_catStats"
17
+ },
18
+ "End": true
19
+ }
20
+ },
21
+ "restrictions": [
22
+ {
23
+ "roleId": "$everyone",
24
+ "allows": [
25
+ "*"
26
+ ]
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,103 @@
1
+ /* eslint-env mocha */
2
+
3
+ const expect = require('chai').expect
4
+
5
+ const ruleToDatetime = require('../lib/components/services/schedule/utils/rule-to-datetime')
6
+ const datetimeToRule = require('../lib/components/services/schedule/utils/datetime-to-rule')
7
+
8
+ describe('Rule to datetime tests', function () {
9
+ it('No options, should default to now', () => {
10
+ const now = new Date()
11
+ const datetime = ruleToDatetime()
12
+
13
+ expect(datetime.getFullYear()).to.eql(now.getFullYear())
14
+ expect(datetime.getMonth()).to.eql(now.getMonth())
15
+ expect(datetime.getDate()).to.eql(now.getDate())
16
+ expect(datetime.getHours()).to.eql(now.getHours())
17
+ expect(datetime.getMinutes()).to.eql(now.getMinutes())
18
+ expect(datetime.getSeconds()).to.eql(now.getSeconds())
19
+ })
20
+
21
+ it('Year only, the rest defaults to now', () => {
22
+ const now = new Date()
23
+ const year = now.getFullYear() + 2
24
+
25
+ const datetime = ruleToDatetime({ year })
26
+
27
+ expect(datetime.getFullYear()).to.eql(year)
28
+ expect(datetime.getMonth()).to.eql(now.getMonth())
29
+ expect(datetime.getDate()).to.eql(now.getDate())
30
+ expect(datetime.getHours()).to.eql(now.getHours())
31
+ expect(datetime.getMinutes()).to.eql(now.getMinutes())
32
+ expect(datetime.getSeconds()).to.eql(now.getSeconds())
33
+ })
34
+
35
+ it('All options passed in as integers', () => {
36
+ const rule = {
37
+ year: 2020,
38
+ month: 1,
39
+ date: 2,
40
+ hour: 12,
41
+ minute: 30,
42
+ second: 0
43
+ }
44
+
45
+ const datetime = ruleToDatetime(rule)
46
+
47
+ expect(datetime.getFullYear()).to.eql(rule.year)
48
+ expect(datetime.getMonth()).to.eql(rule.month)
49
+ expect(datetime.getDate()).to.eql(rule.date)
50
+ expect(datetime.getHours()).to.eql(rule.hour)
51
+ expect(datetime.getMinutes()).to.eql(rule.minute)
52
+ expect(datetime.getSeconds()).to.eql(rule.second)
53
+ })
54
+
55
+ it('All options passed in as strings', () => {
56
+ const rule = {
57
+ year: '2020',
58
+ month: '1',
59
+ date: '2',
60
+ hour: '12',
61
+ minute: '30',
62
+ second: '0'
63
+ }
64
+
65
+ const datetime = ruleToDatetime(rule)
66
+
67
+ expect(datetime.getFullYear().toString()).to.eql(rule.year)
68
+ expect(datetime.getMonth().toString()).to.eql(rule.month)
69
+ expect(datetime.getDate().toString()).to.eql(rule.date)
70
+ expect(datetime.getHours().toString()).to.eql(rule.hour)
71
+ expect(datetime.getMinutes().toString()).to.eql(rule.minute)
72
+ expect(datetime.getSeconds().toString()).to.eql(rule.second)
73
+ })
74
+ })
75
+
76
+ describe('Datetime to rule tests', function () {
77
+ it('No options, should default to now', () => {
78
+ const now = new Date()
79
+ const rule = datetimeToRule()
80
+
81
+ expect(rule.year).to.eql(now.getFullYear())
82
+ expect(rule.month).to.eql(now.getMonth())
83
+ expect(rule.date).to.eql(now.getDate())
84
+ expect(rule.hour).to.eql(now.getHours())
85
+ expect(rule.minute).to.eql(now.getMinutes())
86
+ expect(rule.second).to.eql(now.getSeconds())
87
+ })
88
+
89
+ it('Specific year, the rest should default to now', () => {
90
+ const datetime = new Date()
91
+ const year = datetime.getFullYear() + 2
92
+ datetime.setFullYear(year)
93
+
94
+ const rule = datetimeToRule(datetime)
95
+
96
+ expect(rule.year).to.eql(year)
97
+ expect(rule.month).to.eql(datetime.getMonth())
98
+ expect(rule.date).to.eql(datetime.getDate())
99
+ expect(rule.hour).to.eql(datetime.getHours())
100
+ expect(rule.minute).to.eql(datetime.getMinutes())
101
+ expect(rule.second).to.eql(datetime.getSeconds())
102
+ })
103
+ })
@@ -0,0 +1,198 @@
1
+ /* eslint-env mocha */
2
+
3
+ const expect = require('chai').expect
4
+ const tymly = require('@wmfs/tymly')
5
+ const path = require('path')
6
+
7
+ const scheduleKey = 'tymlyTest_twoSecondCatUpdates'
8
+
9
+ describe('Tymly schedule tests', function () {
10
+ this.timeout(60000) // 60 second timeout
11
+
12
+ before(function () {
13
+ if (process.env.PG_CONNECTION_STRING && !/^postgres:\/\/[^:]+:[^@]+@(?:localhost|127\.0\.0\.1).*$/.test(process.env.PG_CONNECTION_STRING)) {
14
+ console.log(`Skipping tests due to unsafe PG_CONNECTION_STRING value (${process.env.PG_CONNECTION_STRING})`)
15
+ this.skip()
16
+ }
17
+ })
18
+
19
+ let tymlyService
20
+ let scheduleService
21
+ // let statebox
22
+ let catStatsModel
23
+ // let executionsModel
24
+ let taskModel
25
+ let client
26
+
27
+ let lastRunCount
28
+
29
+ it('boot Tymly', async () => {
30
+ const tymlyServices = await tymly.boot(
31
+ {
32
+ blueprintPaths: [
33
+ path.resolve(__dirname, './fixtures/cats-blueprint')
34
+ ],
35
+ pluginPaths: [
36
+ path.resolve(__dirname, '..'),
37
+ require.resolve('@wmfs/tymly-pg-plugin')
38
+ ]
39
+ }
40
+ )
41
+
42
+ tymlyService = tymlyServices.tymly
43
+ scheduleService = tymlyServices.schedule
44
+ // statebox = tymlyServices.statebox
45
+ taskModel = tymlyServices.storage.models.schedule_task
46
+ catStatsModel = tymlyServices.storage.models.tymlyTest_catStats
47
+ // executionsModel = tymlyServices.storage.models.tymly_execution
48
+ client = tymlyServices.storage.client
49
+
50
+ const catStats = await catStatsModel.find({})
51
+ expect(catStats.length).to.eql(0)
52
+ })
53
+
54
+ it('check the task has persisted to the database', async () => {
55
+ const tasks = await taskModel.find({})
56
+ expect(tasks.length).to.eql(1)
57
+ expect(tasks[0].scheduleType).to.eql('interval')
58
+ expect(tasks[0].interval).to.eql('*/2 * * * * *')
59
+ expect(tasks[0].datetime).to.eql(null)
60
+ expect(tasks[0].status).to.eql('STARTED')
61
+ })
62
+
63
+ it('wait 10 seconds, let the updates run for a bit', done => setTimeout(done, 10000))
64
+
65
+ it('stop cat updates', async () => {
66
+ await scheduleService.stopTask(scheduleKey)
67
+ })
68
+
69
+ it('check the state machine has run multiple times', async () => {
70
+ const tasks = await taskModel.find({})
71
+ expect(tasks.length).to.eql(1)
72
+ expect(tasks[0].scheduleType).to.eql('interval')
73
+ expect(tasks[0].interval).to.eql('*/2 * * * * *')
74
+ expect(tasks[0].datetime).to.eql(null)
75
+ expect(tasks[0].status).to.eql('STOPPED')
76
+
77
+ const { totalRunCount } = tasks[0]
78
+
79
+ lastRunCount = totalRunCount
80
+
81
+ // Waited 10 seconds and runs every 2 seconds so can be 5 or 6
82
+ expect(lastRunCount).to.be.greaterThanOrEqual(5)
83
+ expect(lastRunCount).to.be.lessThanOrEqual(6)
84
+
85
+ const catStats = await catStatsModel.find({})
86
+ expect(catStats.length).to.eql(lastRunCount)
87
+ })
88
+
89
+ it('wait 5 seconds', done => setTimeout(done, 5000))
90
+
91
+ it('ensure scheduled task has not run again', async () => {
92
+ const tasks = await taskModel.find({})
93
+ expect(tasks.length).to.eql(1)
94
+ expect(tasks[0].scheduleType).to.eql('interval')
95
+ expect(tasks[0].interval).to.eql('*/2 * * * * *')
96
+ expect(tasks[0].datetime).to.eql(null)
97
+ expect(tasks[0].status).to.eql('STOPPED')
98
+
99
+ expect(tasks[0].totalRunCount).to.eql(lastRunCount)
100
+
101
+ const catStats = await catStatsModel.find({})
102
+ expect(catStats.length).to.eql(lastRunCount)
103
+ })
104
+
105
+ it('update task to specific date/time', async () => {
106
+ const datetime = new Date()
107
+ datetime.setSeconds(datetime.getSeconds() + 10)
108
+
109
+ await scheduleService.stopAndUpdateTaskSchedule(
110
+ scheduleKey,
111
+ {
112
+ year: datetime.getFullYear(),
113
+ month: datetime.getMonth(),
114
+ date: datetime.getDate(),
115
+ hour: datetime.getHours(),
116
+ minute: datetime.getMinutes(),
117
+ second: datetime.getSeconds()
118
+ }
119
+ )
120
+
121
+ await scheduleService.startTask(scheduleKey)
122
+ })
123
+
124
+ it('wait 15 seconds', done => setTimeout(done, 15000))
125
+
126
+ it('check the state machine has run once more', async () => {
127
+ lastRunCount++
128
+
129
+ const tasks = await taskModel.find({})
130
+ expect(tasks.length).to.eql(1)
131
+ expect(tasks[0].interval).to.eql(null)
132
+ expect(tasks[0].datetime).to.not.eql(null)
133
+ expect(tasks[0].scheduleType).to.eql('datetime')
134
+ expect(tasks[0].totalRunCount).to.eql(lastRunCount)
135
+ expect(tasks[0].status).to.eql('STARTED')
136
+
137
+ const catStats = await catStatsModel.find({})
138
+ expect(catStats.length).to.eql(lastRunCount)
139
+ })
140
+
141
+ it('wait 10 seconds', done => setTimeout(done, 10000))
142
+
143
+ it('ensure scheduled task has not run again', async () => {
144
+ const tasks = await taskModel.find({})
145
+ expect(tasks.length).to.eql(1)
146
+ expect(tasks[0].status).to.eql('STARTED')
147
+
148
+ expect(tasks[0].totalRunCount).to.eql(lastRunCount)
149
+
150
+ const catStats = await catStatsModel.find({})
151
+ expect(catStats.length).to.eql(lastRunCount)
152
+ })
153
+
154
+ it('shutdown Tymly', async () => {
155
+ await tymlyService.shutdown()
156
+ })
157
+
158
+ it('re-boot tymly', async () => {
159
+ const tymlyServices = await tymly.boot(
160
+ {
161
+ blueprintPaths: [
162
+ path.resolve(__dirname, './fixtures/cats-blueprint')
163
+ ],
164
+ pluginPaths: [
165
+ path.resolve(__dirname, '..'),
166
+ require.resolve('@wmfs/tymly-pg-plugin')
167
+ ]
168
+ }
169
+ )
170
+
171
+ tymlyService = tymlyServices.tymly
172
+ scheduleService = tymlyServices.schedule
173
+ taskModel = tymlyServices.storage.models.schedule_task
174
+ catStatsModel = tymlyServices.storage.models.tymlyTest_catStats
175
+ client = tymlyServices.storage.client
176
+ })
177
+
178
+ it('check blueprint tasks not overwritten updates', async () => {
179
+ const tasks = await taskModel.find({})
180
+ expect(tasks.length).to.eql(1)
181
+ expect(tasks[0].key).to.eql(scheduleKey)
182
+ expect(tasks[0].stateMachineName).to.eql('tymlyTest_sendCatUpdate')
183
+ expect(tasks[0].scheduleType).to.eql('datetime')
184
+ expect(tasks[0].interval).to.eql(null)
185
+ expect(tasks[0].status).to.eql('STARTED')
186
+ expect(tasks[0].totalRunCount).to.eql(lastRunCount)
187
+ })
188
+
189
+ it('clean up data', async () => {
190
+ await client.query('DROP SCHEMA tymly CASCADE;')
191
+ await client.query('DROP SCHEMA tymly_test CASCADE;')
192
+ await client.query('DROP SCHEMA schedule CASCADE;')
193
+ })
194
+
195
+ it('shutdown Tymly again', async () => {
196
+ await tymlyService.shutdown()
197
+ })
198
+ })
package/version_plugin ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+
3
+ . .version
4
+ echo $VERSION
5
+ version=$VERSION
6
+ sudo apt-get install jq
7
+ sudo jq --arg version "$version" '.version=$version' plugin.json > plugin_edit.json && mv plugin_edit.json plugin.json
8
+ git config --global user.name "$GIT_COMMITTER_NAME"
9
+ git config --global user.email "$GIT_COMMITTER_EMAIL"
10
+ git remote set-url origin https://${GIT_COMMITTER_NAME}:${GH_TOKEN}@github.com/wmfs/tymly-schedule-plugin.git
11
+ git checkout master
12
+ git add plugin.json
13
+ git commit -m "chore(version): plugin $version [skip ci]"
14
+ git push