agendash 1.0.0 → 3.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.
Files changed (72) hide show
  1. package/Dockerfile +3 -2
  2. package/History.md +80 -58
  3. package/LICENSE +1 -1
  4. package/README.md +173 -49
  5. package/all-jobs.png +0 -0
  6. package/app.js +12 -7
  7. package/bin/agendash-standalone-fastify.js +61 -0
  8. package/bin/agendash-standalone-hapi.js +60 -0
  9. package/bin/agendash-standalone-koa.js +56 -0
  10. package/bin/agendash-standalone.js +47 -15
  11. package/create-job.png +0 -0
  12. package/entrypoint.sh +1 -1
  13. package/lib/controllers/agendash.js +340 -0
  14. package/lib/middlewares/README.md +12 -5
  15. package/lib/middlewares/express.js +58 -34
  16. package/lib/middlewares/fastify.js +75 -0
  17. package/lib/middlewares/hapi.js +65 -88
  18. package/lib/middlewares/koa.js +71 -0
  19. package/mobile-ui-sm.png +0 -0
  20. package/mobile-ui-xs.png +0 -0
  21. package/package.json +45 -36
  22. package/public/android-chrome-192x192.png +0 -0
  23. package/public/android-chrome-512x512.png +0 -0
  24. package/public/app/assets/ArgensdashV2Logo.svg +18 -0
  25. package/public/app/css/styles.css +221 -0
  26. package/public/app/js/confirmDelete.js +40 -0
  27. package/public/app/js/confirmDeleteMulti.js +45 -0
  28. package/public/app/js/confirmRequeue.js +40 -0
  29. package/public/app/js/confirmRequeueMulti.js +45 -0
  30. package/public/app/js/confirms.js +8 -0
  31. package/public/app/js/jobdetail.js +48 -0
  32. package/public/app/js/joblist.js +239 -0
  33. package/public/app/js/main.js +275 -0
  34. package/public/app/js/newJob.js +89 -0
  35. package/public/app/js/sidebar.js +121 -0
  36. package/public/app/js/topbar.js +95 -0
  37. package/public/apple-touch-icon.png +0 -0
  38. package/public/favicon-16x16.png +0 -0
  39. package/public/favicon-32x32.png +0 -0
  40. package/public/favicon.ico +0 -0
  41. package/public/index.html +40 -164
  42. package/public/site.webmanifest +1 -0
  43. package/search.png +0 -0
  44. package/.editorconfig +0 -7
  45. package/.travis.yml +0 -24
  46. package/job-details.png +0 -0
  47. package/lib/agendash.js +0 -295
  48. package/public/css/bootstrap-theme.css +0 -587
  49. package/public/css/bootstrap-theme.css.map +0 -1
  50. package/public/css/bootstrap-theme.min.css +0 -6
  51. package/public/css/bootstrap-theme.min.css.map +0 -1
  52. package/public/css/bootstrap.css +0 -6760
  53. package/public/css/bootstrap.css.map +0 -1
  54. package/public/css/bootstrap.min.css +0 -6
  55. package/public/css/bootstrap.min.css.map +0 -1
  56. package/public/css/dashboard.css +0 -94
  57. package/public/fonts/glyphicons-halflings-regular.eot +0 -0
  58. package/public/fonts/glyphicons-halflings-regular.svg +0 -288
  59. package/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  60. package/public/fonts/glyphicons-halflings-regular.woff +0 -0
  61. package/public/fonts/glyphicons-halflings-regular.woff2 +0 -0
  62. package/public/js/backbone-min.js +0 -2
  63. package/public/js/backbone-min.map +0 -1
  64. package/public/js/bootstrap.js +0 -2363
  65. package/public/js/bootstrap.min.js +0 -7
  66. package/public/js/jquery-2.2.0.min.js +0 -4
  67. package/public/js/lodash.min.js +0 -120
  68. package/public/js/main.js +0 -414
  69. package/public/js/moment.min.js +0 -7
  70. package/test/test-express.js +0 -98
  71. package/test/test-hapi.js +0 -104
  72. package/yarn.lock +0 -4412
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const { Agenda } = require("agenda");
4
+ const agendash = require("../app");
5
+ const Koa = require("koa");
6
+ const program = require("commander");
7
+
8
+ program
9
+ .option(
10
+ "-d, --db <db>",
11
+ "[required] Mongo connection string, same as Agenda connection string"
12
+ )
13
+ .option(
14
+ "-c, --collection <collection>",
15
+ "[optional] Mongo collection, same as Agenda collection name, default agendaJobs",
16
+ "agendaJobs"
17
+ )
18
+ .option(
19
+ "-p, --port <port>",
20
+ "[optional] Server port, default 3000",
21
+ (n, d) => Number(n) || d,
22
+ 3000
23
+ )
24
+ .option(
25
+ "-t, --title <title>",
26
+ "[optional] Page title, default Agendash",
27
+ "Agendash"
28
+ )
29
+ .parse(process.argv);
30
+
31
+ if (!program.db) {
32
+ console.error("--db required");
33
+ process.exit(1);
34
+ }
35
+
36
+ const init = async () => {
37
+ const agenda = new Agenda().database(program.db, program.collection);
38
+
39
+ const app = new Koa();
40
+ const middlewares = agendash(agenda, {
41
+ middleware: "koa",
42
+ });
43
+ for (const middleware of middlewares) {
44
+ app.use(middleware);
45
+ }
46
+
47
+ await app.listen(program.port);
48
+ console.log("Server running on port %s", program.port);
49
+ };
50
+
51
+ process.on("unhandledRejection", (error) => {
52
+ console.log(error);
53
+ process.exit(1);
54
+ });
55
+
56
+ init();
@@ -1,32 +1,64 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
- const http = require('http');
4
- const Agenda = require('agenda');
5
- const express = require('express');
6
- const program = require('commander');
2
+ "use strict";
3
+ const http = require("http");
4
+ const { Agenda } = require("agenda");
5
+ const agendash = require("../app");
6
+ const express = require("express");
7
+ const program = require("commander");
7
8
 
8
9
  program
9
- .option('-d, --db <db>', '[required] Mongo connection string, same as Agenda connection string')
10
- .option('-c, --collection <collection>', '[optional] Mongo collection, same as Agenda collection name, default agendaJobs', 'agendaJobs')
11
- .option('-p, --port <port>', '[optional] Server port, default 3000', (n, d) => Number(n) || d, 3000)
12
- .option('-t, --title <title>', '[optional] Page title, default Agendash', 'Agendash')
10
+ .option(
11
+ "-d, --db <db>",
12
+ "[required] Mongo connection string, same as Agenda connection string"
13
+ )
14
+ .option(
15
+ "-c, --collection <collection>",
16
+ "[optional] Mongo collection, same as Agenda collection name, default agendaJobs",
17
+ "agendaJobs"
18
+ )
19
+ .option(
20
+ "-p, --port <port>",
21
+ "[optional] Server port, default 3000",
22
+ (n, d) => Number(n) || d,
23
+ 3000
24
+ )
25
+ .option(
26
+ "-t, --title <title>",
27
+ "[optional] Page title, default Agendash",
28
+ "Agendash"
29
+ )
30
+ .option(
31
+ "-p, --path <path>",
32
+ "[optional] Path to bind Agendash to, default /",
33
+ "/"
34
+ )
13
35
  .parse(process.argv);
14
36
 
15
37
  if (!program.db) {
16
- console.error('--db required');
38
+ console.error("--db required");
39
+ process.exit(1);
40
+ }
41
+
42
+ if (!program.path.startsWith("/")) {
43
+ console.error("--path must begin with /");
17
44
  process.exit(1);
18
45
  }
19
46
 
20
47
  const app = express();
21
48
 
22
49
  const agenda = new Agenda().database(program.db, program.collection);
23
- app.use('/', require('../app')(agenda, {
24
- title: program.title
25
- }));
50
+ app.use(
51
+ program.path,
52
+ agendash(agenda, {
53
+ title: program.title,
54
+ })
55
+ );
26
56
 
27
- app.set('port', program.port);
57
+ app.set("port", program.port);
28
58
 
29
59
  const server = http.createServer(app);
30
60
  server.listen(program.port, () => {
31
- console.log(`Agendash started http://localhost:${program.port}`);
61
+ console.log(
62
+ `Agendash started http://localhost:${program.port}${program.path}`
63
+ );
32
64
  });
package/create-job.png ADDED
Binary file
package/entrypoint.sh CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  set +e
4
4
 
5
- exec node ./bin/agendash-standalone.js --db=$MONGODB_URI --collection=$COLLECTION
5
+ exec node ./bin/agendash-standalone.js --db=$MONGODB_URI --collection=$COLLECTION --path=$BASE_PATH
@@ -0,0 +1,340 @@
1
+ "use strict";
2
+ const semver = require("semver");
3
+ const { ObjectId } = require("mongodb"); // We rely on the Agenda's "mongodb", thus our package.json lists "*" as required version
4
+
5
+ module.exports = function (agenda, options) {
6
+ options = options || {};
7
+
8
+ agenda.on("ready", () => {
9
+ const collection = agenda._collection.collection || agenda._collection;
10
+ collection.createIndexes(
11
+ [
12
+ { key: { nextRunAt: -1, lastRunAt: -1, lastFinishedAt: -1 } },
13
+ { key: { name: 1, nextRunAt: -1, lastRunAt: -1, lastFinishedAt: -1 } },
14
+ ],
15
+ (error) => {
16
+ if (error) {
17
+ // Ignoring for now
18
+ }
19
+ }
20
+ );
21
+
22
+ // Mongoose internals changed at some point. This will fix crash for older versions.
23
+ const mdb = agenda._mdb.admin ? agenda._mdb : agenda._mdb.db;
24
+
25
+ mdb.admin().serverInfo((error, serverInfo) => {
26
+ if (error) {
27
+ throw error;
28
+ }
29
+
30
+ if (!semver.satisfies(semver.coerce(serverInfo.version), ">=3.6.0")) {
31
+ throw new Error("MongoDB version not supported");
32
+ }
33
+ });
34
+ });
35
+
36
+ // Options = {query = '', property = '', isObjectId = false, limit, skip}
37
+ const getJobs = function (job, state, options) {
38
+ const preMatch = {};
39
+ if (job) {
40
+ preMatch.name = job;
41
+ }
42
+
43
+ if (options.query && options.property) {
44
+ if (options.isObjectId) {
45
+ preMatch[options.property] = ObjectId(options.query);
46
+ } else if (/^\d+$/.test(options.query)) {
47
+ preMatch[options.property] = Number.parseInt(options.query, 10);
48
+ } else {
49
+ preMatch[options.property] = { $regex: options.query, $options: "i" };
50
+ }
51
+ }
52
+
53
+ const postMatch = {};
54
+ if (state) {
55
+ postMatch[state] = true;
56
+ }
57
+
58
+ const collection = agenda._collection.collection || agenda._collection;
59
+ return collection
60
+ .aggregate([
61
+ { $match: preMatch },
62
+ {
63
+ $sort: {
64
+ nextRunAt: -1,
65
+ lastRunAt: -1,
66
+ lastFinishedAt: -1,
67
+ },
68
+ },
69
+ {
70
+ $project: {
71
+ job: "$$ROOT",
72
+ _id: "$$ROOT._id",
73
+ running: {
74
+ $and: ["$lastRunAt", { $gt: ["$lastRunAt", "$lastFinishedAt"] }],
75
+ },
76
+ scheduled: {
77
+ $and: ["$nextRunAt", { $gte: ["$nextRunAt", new Date()] }],
78
+ },
79
+ queued: {
80
+ $and: [
81
+ "$nextRunAt",
82
+ { $gte: [new Date(), "$nextRunAt"] },
83
+ { $gte: ["$nextRunAt", "$lastFinishedAt"] },
84
+ ],
85
+ },
86
+ completed: {
87
+ $and: [
88
+ "$lastFinishedAt",
89
+ { $gt: ["$lastFinishedAt", "$failedAt"] },
90
+ ],
91
+ },
92
+ failed: {
93
+ $and: [
94
+ "$lastFinishedAt",
95
+ "$failedAt",
96
+ { $eq: ["$lastFinishedAt", "$failedAt"] },
97
+ ],
98
+ },
99
+ repeating: {
100
+ $and: ["$repeatInterval", { $ne: ["$repeatInterval", null] }],
101
+ },
102
+ },
103
+ },
104
+ { $match: postMatch },
105
+ {
106
+ $facet: {
107
+ pages: [
108
+ { $count: "totalMatchs" },
109
+ {
110
+ $project: {
111
+ totalPages: {
112
+ $ceil: { $divide: ["$totalMatchs", options.limit] },
113
+ },
114
+ },
115
+ },
116
+ ],
117
+ filtered: [{ $skip: options.skip }, { $limit: options.limit }],
118
+ },
119
+ },
120
+ ])
121
+ .toArray();
122
+ };
123
+
124
+ const getOverview = async () => {
125
+ const collection = agenda._collection.collection || agenda._collection;
126
+ const results = await collection
127
+ .aggregate([
128
+ {
129
+ $group: {
130
+ _id: "$name",
131
+ displayName: { $first: "$name" },
132
+ meta: {
133
+ $addToSet: {
134
+ type: "$type",
135
+ priority: "$priority",
136
+ repeatInterval: "$repeatInterval",
137
+ repeatTimezone: "$repeatTimezone",
138
+ },
139
+ },
140
+ total: { $sum: 1 },
141
+ running: {
142
+ $sum: {
143
+ $cond: [
144
+ {
145
+ $and: [
146
+ "$lastRunAt",
147
+ { $gt: ["$lastRunAt", "$lastFinishedAt"] },
148
+ ],
149
+ },
150
+ 1,
151
+ 0,
152
+ ],
153
+ },
154
+ },
155
+ scheduled: {
156
+ $sum: {
157
+ $cond: [
158
+ {
159
+ $and: ["$nextRunAt", { $gte: ["$nextRunAt", new Date()] }],
160
+ },
161
+ 1,
162
+ 0,
163
+ ],
164
+ },
165
+ },
166
+ queued: {
167
+ $sum: {
168
+ $cond: [
169
+ {
170
+ $and: [
171
+ "$nextRunAt",
172
+ { $gte: [new Date(), "$nextRunAt"] },
173
+ { $gte: ["$nextRunAt", "$lastFinishedAt"] },
174
+ ],
175
+ },
176
+ 1,
177
+ 0,
178
+ ],
179
+ },
180
+ },
181
+ completed: {
182
+ $sum: {
183
+ $cond: [
184
+ {
185
+ $and: [
186
+ "$lastFinishedAt",
187
+ { $gt: ["$lastFinishedAt", "$failedAt"] },
188
+ ],
189
+ },
190
+ 1,
191
+ 0,
192
+ ],
193
+ },
194
+ },
195
+ failed: {
196
+ $sum: {
197
+ $cond: [
198
+ {
199
+ $and: [
200
+ "$lastFinishedAt",
201
+ "$failedAt",
202
+ { $eq: ["$lastFinishedAt", "$failedAt"] },
203
+ ],
204
+ },
205
+ 1,
206
+ 0,
207
+ ],
208
+ },
209
+ },
210
+ repeating: {
211
+ $sum: {
212
+ $cond: [
213
+ {
214
+ $and: [
215
+ "$repeatInterval",
216
+ { $ne: ["$repeatInterval", null] },
217
+ ],
218
+ },
219
+ 1,
220
+ 0,
221
+ ],
222
+ },
223
+ },
224
+ },
225
+ },
226
+ ])
227
+ .toArray();
228
+ const states = {
229
+ total: 0,
230
+ running: 0,
231
+ scheduled: 0,
232
+ queued: 0,
233
+ completed: 0,
234
+ failed: 0,
235
+ repeating: 0,
236
+ };
237
+ const totals = { displayName: "All Jobs", ...states };
238
+
239
+ for (const job of results) {
240
+ for (const state of Object.keys(states)) {
241
+ totals[state] += job[state];
242
+ }
243
+ }
244
+
245
+ results.unshift(totals);
246
+ return results;
247
+ };
248
+
249
+ const api = async function (
250
+ job,
251
+ state,
252
+ { query: q, property, isObjectId, skip, limit }
253
+ ) {
254
+ if (!agenda) {
255
+ return Promise.reject(new Error("Agenda instance is not ready"));
256
+ }
257
+
258
+ limit = Number.parseInt(limit, 10) || 200;
259
+ skip = Number.parseInt(skip, 10) || 0;
260
+
261
+ const [overview, jobs] = await Promise.all([
262
+ getOverview(),
263
+ getJobs(job, state, { query: q, property, isObjectId, skip, limit }),
264
+ ]);
265
+ const apiResponse = {
266
+ overview,
267
+ jobs: jobs[0].filtered,
268
+ totalPages: jobs[0].pages[0] ? jobs[0].pages[0].totalPages : 0,
269
+ };
270
+ apiResponse.title = options.title || "Agendash";
271
+ apiResponse.currentRequest = {
272
+ title: options.title || "Agendash",
273
+ job: job || "All Jobs",
274
+ state,
275
+ };
276
+ return apiResponse;
277
+ };
278
+
279
+ const requeueJobs = async (jobIds) => {
280
+ if (!agenda) {
281
+ return Promise.reject(new Error("Agenda instance is not ready"));
282
+ }
283
+
284
+ const collection = agenda._collection.collection || agenda._collection;
285
+ const jobs = await collection
286
+ .find({
287
+ _id: { $in: jobIds.map((jobId) => ObjectId(jobId)) },
288
+ })
289
+ .toArray();
290
+ if (jobs.length === 0) {
291
+ throw new Error("Job not found");
292
+ }
293
+
294
+ for (const job of jobs) {
295
+ const newJob = agenda.create(job.name, job.data);
296
+ // eslint-disable-next-line no-await-in-loop
297
+ await newJob.save();
298
+ }
299
+
300
+ return "Jobs create successfully";
301
+ };
302
+
303
+ const deleteJobs = (jobIds) => {
304
+ if (!agenda) {
305
+ return Promise.reject(new Error("Agenda instance is not ready"));
306
+ }
307
+
308
+ return agenda.cancel({
309
+ _id: { $in: jobIds.map((jobId) => ObjectId(jobId)) },
310
+ });
311
+ };
312
+
313
+ const createJob = (jobName, jobSchedule, jobRepeatEvery, jobData) => {
314
+ if (!agenda) {
315
+ return Promise.reject(new Error("Agenda instance is not ready"));
316
+ }
317
+
318
+ // @TODO: Need to validate user input.
319
+ const job = agenda.create(jobName, jobData);
320
+ if (jobSchedule && jobRepeatEvery) {
321
+ job.repeatAt(jobSchedule);
322
+ job.repeatEvery(jobRepeatEvery);
323
+ } else if (jobSchedule) {
324
+ job.schedule(jobSchedule);
325
+ } else if (jobRepeatEvery) {
326
+ job.repeatEvery(jobRepeatEvery);
327
+ } else {
328
+ return Promise.reject(new Error("Jobs not created"));
329
+ }
330
+
331
+ return job.save();
332
+ };
333
+
334
+ return {
335
+ api,
336
+ requeueJobs,
337
+ deleteJobs,
338
+ createJob,
339
+ };
340
+ };
@@ -1,17 +1,24 @@
1
1
  # Middlewares
2
+
2
3
  ## Hapi
3
- Works with [Hapijs](https://hapijs.com/) >= v17
4
+
5
+ Works with [Hapijs](https://hapijs.com/) >= v18
6
+
4
7
  ### usage
8
+
5
9
  First install the dependencies in your app:
10
+
6
11
  ```bash
7
- npm install hapi inert agenda agendash
12
+ npm install @hapi/hapi @hapi/inert agenda agendash
8
13
  ```
14
+
9
15
  Then use it as follows:
16
+
10
17
  ```javascript
11
18
  'use strict';
12
19
 
13
20
  const Hapi = require('hapi');
14
- const Agenda = require('agenda');
21
+ const { Agenda } = require('agenda');
15
22
  const Agendash = require('agendash');
16
23
 
17
24
  var agenda = new Agenda({
@@ -26,7 +33,7 @@ const server = Hapi.server({
26
33
  const init = async () => {
27
34
 
28
35
  await server.register(
29
- require('inert'),
36
+ require('@hapi/inert'),
30
37
  {
31
38
  plugin: Agendash(agenda, {
32
39
  middleware: 'hapi'
@@ -49,4 +56,4 @@ process.on('unhandledRejection', (err) => {
49
56
  });
50
57
 
51
58
  init();
52
- ```
59
+ ```
@@ -1,48 +1,72 @@
1
- 'use strict';
2
- const path = require('path');
3
- const express = require('express');
4
- const bodyParser = require('body-parser');
1
+ const path = require("path");
2
+ const express = require("express");
3
+ const bodyParser = require("body-parser");
5
4
 
6
- module.exports = agendash => {
5
+ module.exports = (agendash) => {
6
+ const { api, requeueJobs, deleteJobs, createJob } = agendash;
7
7
  const app = express();
8
8
  app.use(bodyParser.json());
9
- app.use(bodyParser.urlencoded({extended: false}));
10
- app.use('/', express.static(path.join(__dirname, '../../public')));
9
+ app.use(bodyParser.urlencoded({ extended: false }));
10
+ app.use("/", express.static(path.join(__dirname, "../../public")));
11
11
 
12
- app.get('/api', (req, res) => {
13
- agendash.api(req.query.job, req.query.state, (err, apiResponse) => {
14
- if (err) {
15
- return res.status(400).json(err);
16
- }
17
- res.json(apiResponse);
18
- });
12
+ app.get("/api", async (request, response) => {
13
+ try {
14
+ const {
15
+ job,
16
+ state,
17
+ skip,
18
+ limit,
19
+ q,
20
+ property,
21
+ isObjectId,
22
+ } = request.query;
23
+ const apiResponse = await api(job, state, {
24
+ query: q,
25
+ property,
26
+ isObjectId,
27
+ skip,
28
+ limit,
29
+ });
30
+ response.json(apiResponse);
31
+ } catch (error) {
32
+ response.status(400).json(error);
33
+ }
19
34
  });
20
35
 
21
- app.post('/api/jobs/requeue', (req, res) => {
22
- agendash.requeueJobs(req.body.jobIds, (err, newJobs) => {
23
- if (err || !newJobs) {
24
- return res.status(404).json(err);
25
- }
26
- res.json(newJobs);
27
- });
36
+ app.post("/api/jobs/requeue", async (request, response) => {
37
+ try {
38
+ const newJobs = await requeueJobs(request.body.jobIds);
39
+ response.send(newJobs);
40
+ } catch (error) {
41
+ response.status(404).json(error);
42
+ }
28
43
  });
29
44
 
30
- app.post('/api/jobs/delete', (req, res) => {
31
- agendash.deleteJobs(req.body.jobIds, err => {
32
- if (err) {
33
- return res.status(404).json(err);
45
+ app.post("/api/jobs/delete", async (request, response) => {
46
+ try {
47
+ const deleted = await deleteJobs(request.body.jobIds);
48
+ if (deleted) {
49
+ response.json({ deleted: true });
50
+ } else {
51
+ response.json({ message: "Jobs not deleted" });
34
52
  }
35
- return res.json({deleted: true});
36
- });
53
+ } catch (error) {
54
+ response.status(404).json(error);
55
+ }
37
56
  });
38
57
 
39
- app.post('/api/jobs/create', (req, res) => {
40
- agendash.createJob(req.body.jobName, req.body.jobSchedule, req.body.jobRepeatEvery, req.body.jobData, err => {
41
- if (err) {
42
- return res.status(404).json(err);
43
- }
44
- return res.json({created: true});
45
- });
58
+ app.post("/api/jobs/create", async (request, response) => {
59
+ try {
60
+ await createJob(
61
+ request.body.jobName,
62
+ request.body.jobSchedule,
63
+ request.body.jobRepeatEvery,
64
+ request.body.jobData
65
+ );
66
+ response.json({ created: true });
67
+ } catch (error) {
68
+ response.status(400).json(error);
69
+ }
46
70
  });
47
71
 
48
72
  return app;