├── LICENSE
├── README.md
├── index.js
├── package-lock.json
├── package.json
└── scheduler.jpg
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Coder of Salvation / Leon van Kammen
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Rolled your own parse-server, and realized scheduled jobs don't work out of the box?
2 |
3 | ## Usage
4 |
5 | run `npm install parse-server-scheduler` in your `cloud/index.js` or `app.js` simply include:
6 |
7 | ```
8 | require('parse-server-scheduler')(Parse)
9 | ```
10 |
11 | > Voila! Profit!
12 |
13 | ## Why
14 |
15 | 
16 |
17 | Parse exposes scheduled jobs as HTTP endpoints, which is great and a disappointment at the same time :)
18 | This empowers your server with an internal scheduler using the `cron` and `moment` npm-packages.
19 |
20 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const cron = require('cron')
2 | const moment = require('moment')
3 | const Parse = require('parse/node')
4 | const rp = require('request-promise')
5 |
6 | const CronJob = cron.CronJob
7 | const PARSE_TIMEZONE = 'UTC'
8 |
9 | let cronJobs = {}
10 |
11 | module.exports = (Parse) => {
12 |
13 | /**
14 | * Parse job schedule object
15 | * @typedef {Object} _JobSchedule
16 | * @property {String} id The job id
17 | */
18 |
19 | /**
20 | * Recreate the cron schedules for a specific _JobSchedule or all _JobSchedule objects
21 | * @param {_JobSchedule | string} [job=null] The job schedule to recreate. If not specified, all jobs schedules will be recreated.
22 | * Can be a _JobSchedule object or the id of a _JobSchedule object.
23 | */
24 | const recreateSchedule = async (job) => {
25 | if (job) {
26 | if (job instanceof String || typeof job === 'string') {
27 | try {
28 | const jobObject = await Parse.Object.extend('_JobSchedule').createWithoutData(job).fetch({
29 | useMasterKey: true
30 | })
31 | if (jobObject) {
32 | recreateJobSchedule(jobObject)
33 | } else {
34 | throw new Error(`No _JobSchedule was found with id ${job}`)
35 | }
36 | } catch (error) {
37 | throw error
38 | }
39 | } else if (job instanceof Parse.Object && job.className === '_JobSchedule') {
40 | recreateJobSchedule(job)
41 | } else {
42 | throw new Error('Invalid job type. Must be a string or a _JobSchedule')
43 | }
44 | } else {
45 | try {
46 | recreateScheduleForAllJobs()
47 | } catch (error) {
48 | throw error
49 | }
50 | }
51 | }
52 |
53 | /**
54 | * (Re)creates all schedules (crons) for all _JobSchedule from the Parse server
55 | */
56 | const recreateScheduleForAllJobs = async () => {
57 | if (!Parse.applicationId) {
58 | throw new Error('Parse is not initialized')
59 | }
60 |
61 | try {
62 | const results = await new Parse.Query('_JobSchedule').find({
63 | useMasterKey: true
64 | })
65 |
66 | destroySchedules()
67 |
68 | for (let job of results) {
69 | try {
70 | recreateJobSchedule(job)
71 | } catch (error) {
72 | console.log(error)
73 | }
74 | }
75 | console.log(`${Object.keys(cronJobs).length} job(s) scheduled.`)
76 | } catch (error) {
77 | throw error
78 | }
79 | }
80 |
81 | /**
82 | * (Re)creates the schedule (crons) of a _JobSchedule
83 | * @param {_JobSchedule} job The _JobSchedule
84 | */
85 | const recreateJobSchedule = (job) => {
86 | destroySchedule(job.id)
87 | cronJobs[job.id] = createCronJobs(job)
88 | }
89 |
90 | /**
91 | * Stop all jobs and remove them from the list of jobs
92 | */
93 | const destroySchedules = () => {
94 | for (let key of Object.keys(cronJobs)) {
95 | destroySchedule(key)
96 | }
97 | cronJobs = {}
98 | }
99 |
100 | /**
101 | * Destroy a planned cron job
102 | * @param {String} id The _JobSchedule id
103 | */
104 | const destroySchedule = (id) => {
105 | const jobs = cronJobs[id]
106 | if (jobs) {
107 | for (let job of jobs) {
108 | job.stop()
109 | }
110 | delete cronJobs[id]
111 | }
112 | }
113 |
114 | const createCronJobs = (job) => {
115 | const startDate = new Date(job.get('startAfter'))
116 | const repeatMinutes = job.get('repeatMinutes')
117 | const jobName = job.get('jobName')
118 | const params = job.get('params')
119 | const now = moment()
120 |
121 | // Launch just once
122 | if (!repeatMinutes) {
123 | return [
124 | new CronJob(
125 | startDate,
126 | () => { // On tick
127 | performJob(jobName, params)
128 | },
129 | null, // On complete
130 | true, // Start
131 | PARSE_TIMEZONE // Timezone
132 | )
133 | ]
134 | }
135 | // Periodic job. Create a cron to launch the periodic job a the start date.
136 | let timeOfDay = moment(job.get('timeOfDay'), 'HH:mm:ss.Z').utc()
137 | const daysOfWeek = job.get('daysOfWeek')
138 | const cronDoW = (daysOfWeek) ? daysOfWeekToCronString(daysOfWeek) : '*'
139 | const minutes = repeatMinutes % 60
140 | const hours = Math.floor(repeatMinutes / 60)
141 |
142 | let cron = '0 '
143 | // Minutes
144 | if (minutes) {
145 | cron += `${timeOfDay.minutes()}-59/${minutes} `
146 | } else {
147 | cron += `0 `
148 | }
149 |
150 | // Hours
151 | cron += `${timeOfDay.hours()}-23`
152 | if (hours) {
153 | cron += `/${hours}`
154 | }
155 | cron += ' '
156 |
157 | // Day of month
158 | cron += '* '
159 |
160 | // Month
161 | cron += '* '
162 |
163 | // Days of week
164 | cron += cronDoW
165 |
166 | console.log(`${jobName}: ${cron}`)
167 |
168 | const actualJob = new CronJob(
169 | cron,
170 | () => { // On tick
171 | performJob(jobName, params)
172 | },
173 | null, // On complete
174 | false, // Start
175 | PARSE_TIMEZONE // Timezone
176 | )
177 |
178 | // If startDate is before now, start the cron now
179 | if (moment(startDate).isBefore(now)) {
180 | actualJob.start()
181 | return [actualJob]
182 | }
183 |
184 | // Otherwise, schedule a cron that is going to launch our actual cron at the time of the day
185 | const startCron = new CronJob(
186 | startDate,
187 | () => { // On tick
188 | console.log('Start the cron')
189 | actualJob.start()
190 | },
191 | null, // On complete
192 | true, // Start
193 | PARSE_TIMEZONE // Timezone
194 | )
195 |
196 | return [startCron, actualJob]
197 | }
198 |
199 | /**
200 | * Converts the Parse scheduler days of week
201 | * @param {Array} daysOfWeek An array of seven elements for the days of the week. 1 to schedule the task for the day, otherwise 0.
202 | */
203 | const daysOfWeekToCronString = (daysOfWeek) => {
204 | const daysNumbers = []
205 | for (let i = 0; i < daysOfWeek.length; i++) {
206 | if (daysOfWeek[i]) {
207 | daysNumbers.push((i + 1) % 7)
208 | }
209 | }
210 | return daysNumbers.join(',')
211 | }
212 |
213 | /**
214 | * Perform a background job
215 | * @param {String} jobName The job name on Parse Server
216 | * @param {Object=} params The parameters to pass to the request
217 | */
218 | const performJob = async (jobName, params) => {
219 | try {
220 | const request = rp({
221 | method: 'POST',
222 | uri: Parse.serverURL + '/jobs/' + jobName,
223 | headers: {
224 | 'X-Parse-Application-Id': Parse.applicationId,
225 | 'X-Parse-Master-Key': Parse.masterKey
226 | },
227 | json: true // Automatically parses the JSON string in the response
228 | })
229 | if (params) {
230 | request.body = params
231 | }
232 | console.log(`Job ${jobName} launched.`)
233 | } catch (error) {
234 | console.log(error)
235 | }
236 | }
237 |
238 | const init = () => {
239 |
240 | // Recreates all crons when the server is launched
241 | recreateSchedule()
242 |
243 | // Recreates schedule when a job schedule has changed
244 | Parse.Cloud.afterSave('_JobSchedule', async (request) => {
245 | recreateSchedule(request.object)
246 | })
247 |
248 | // Destroy schedule for removed job
249 | Parse.Cloud.afterDelete('_JobSchedule', async (request) => {
250 | destroySchedule(request.object.id)
251 | })
252 | }
253 |
254 | init()
255 |
256 | }
257 |
258 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "parse-server-scheduler",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@babel/runtime": {
8 | "version": "7.9.2",
9 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz",
10 | "integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==",
11 | "requires": {
12 | "regenerator-runtime": "^0.13.4"
13 | }
14 | },
15 | "@babel/runtime-corejs3": {
16 | "version": "7.9.2",
17 | "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.9.2.tgz",
18 | "integrity": "sha512-HHxmgxbIzOfFlZ+tdeRKtaxWOMUoCG5Mu3wKeUmOxjYrwb3AAHgnmtCUbPPK11/raIWLIBK250t8E2BPO0p7jA==",
19 | "requires": {
20 | "core-js-pure": "^3.0.0",
21 | "regenerator-runtime": "^0.13.4"
22 | }
23 | },
24 | "bluebird": {
25 | "version": "3.7.2",
26 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
27 | "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
28 | },
29 | "core-js-pure": {
30 | "version": "3.6.5",
31 | "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz",
32 | "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA=="
33 | },
34 | "cron": {
35 | "version": "1.8.2",
36 | "resolved": "https://registry.npmjs.org/cron/-/cron-1.8.2.tgz",
37 | "integrity": "sha512-Gk2c4y6xKEO8FSAUTklqtfSr7oTq0CiPQeLBG5Fl0qoXpZyMcj1SG59YL+hqq04bu6/IuEA7lMkYDAplQNKkyg==",
38 | "requires": {
39 | "moment-timezone": "^0.5.x"
40 | }
41 | },
42 | "crypto-js": {
43 | "version": "4.0.0",
44 | "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz",
45 | "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg=="
46 | },
47 | "lodash": {
48 | "version": "4.17.15",
49 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
50 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
51 | },
52 | "moment": {
53 | "version": "2.26.0",
54 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz",
55 | "integrity": "sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw=="
56 | },
57 | "moment-timezone": {
58 | "version": "0.5.31",
59 | "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz",
60 | "integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==",
61 | "requires": {
62 | "moment": ">= 2.9.0"
63 | }
64 | },
65 | "parse": {
66 | "version": "2.13.0",
67 | "resolved": "https://registry.npmjs.org/parse/-/parse-2.13.0.tgz",
68 | "integrity": "sha512-0W7FBmtjtVpaxf+AK8S2+o6rosV4vzzvB9ba/8Y+7wWNPiy/9L8s+F0gI5ADYx/KbRqF0uR/9Q+FzbJB6IZ0Sw==",
69 | "requires": {
70 | "@babel/runtime": "7.9.2",
71 | "@babel/runtime-corejs3": "7.9.2",
72 | "crypto-js": "4.0.0",
73 | "react-native-crypto-js": "1.0.0",
74 | "uuid": "3.3.3",
75 | "ws": "7.2.5",
76 | "xmlhttprequest": "1.8.0"
77 | }
78 | },
79 | "psl": {
80 | "version": "1.8.0",
81 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
82 | "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
83 | },
84 | "punycode": {
85 | "version": "2.1.1",
86 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
87 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
88 | },
89 | "react-native-crypto-js": {
90 | "version": "1.0.0",
91 | "resolved": "https://registry.npmjs.org/react-native-crypto-js/-/react-native-crypto-js-1.0.0.tgz",
92 | "integrity": "sha512-FNbLuG/HAdapQoybeZSoes1PWdOj0w242gb+e1R0hicf3Gyj/Mf8M9NaED2AnXVOX01b2FXomwUiw1xP1K+8sA=="
93 | },
94 | "regenerator-runtime": {
95 | "version": "0.13.5",
96 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
97 | "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
98 | },
99 | "request-promise": {
100 | "version": "4.2.5",
101 | "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.5.tgz",
102 | "integrity": "sha512-ZgnepCykFdmpq86fKGwqntyTiUrHycALuGggpyCZwMvGaZWgxW6yagT0FHkgo5LzYvOaCNvxYwWYIjevSH1EDg==",
103 | "requires": {
104 | "bluebird": "^3.5.0",
105 | "request-promise-core": "1.1.3",
106 | "stealthy-require": "^1.1.1",
107 | "tough-cookie": "^2.3.3"
108 | }
109 | },
110 | "request-promise-core": {
111 | "version": "1.1.3",
112 | "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz",
113 | "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==",
114 | "requires": {
115 | "lodash": "^4.17.15"
116 | }
117 | },
118 | "stealthy-require": {
119 | "version": "1.1.1",
120 | "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
121 | "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
122 | },
123 | "tough-cookie": {
124 | "version": "2.5.0",
125 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
126 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
127 | "requires": {
128 | "psl": "^1.1.28",
129 | "punycode": "^2.1.1"
130 | }
131 | },
132 | "uuid": {
133 | "version": "3.3.3",
134 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
135 | "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ=="
136 | },
137 | "ws": {
138 | "version": "7.2.5",
139 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.5.tgz",
140 | "integrity": "sha512-C34cIU4+DB2vMyAbmEKossWq2ZQDr6QEyuuCzWrM9zfw1sGc0mYiJ0UnG9zzNykt49C2Fi34hvr2vssFQRS6EA=="
141 | },
142 | "xmlhttprequest": {
143 | "version": "1.8.0",
144 | "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz",
145 | "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw="
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "parse-server-scheduler",
3 | "version": "1.0.21",
4 | "description": "get scheduled jobs to run automatically (without external server) in parse",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/coderofsalvation/parse-server-scheduler.git"
12 | },
13 | "author": "",
14 | "license": "ISC",
15 | "bugs": {
16 | "url": "https://github.com/coderofsalvation/parse-server-scheduler/issues"
17 | },
18 | "homepage": "https://github.com/coderofsalvation/parse-server-scheduler#readme",
19 | "dependencies": {
20 | "cron": "^1.8.2",
21 | "moment": "^2.26.0",
22 | "parse": "^2.13.0",
23 | "request-promise": "^4.2.5"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/scheduler.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderofsalvation/parse-server-scheduler/86a9aa70b6cb2490b414472d315aac1adb5b6f8c/scheduler.jpg
--------------------------------------------------------------------------------