Browse Source

regularly delete abandoned tags + refactor

mrkvon 4 years ago
parent
commit
e6438c99c8
8 changed files with 178 additions and 65 deletions
  1. 5
    5
      app.js
  2. 9
    1
      bin/www
  3. 23
    0
      jobs/index.js
  4. 11
    0
      jobs/tags.js
  5. 5
    5
      models/index.js
  6. 79
    42
      models/tag/index.js
  7. 1
    0
      package.json
  8. 45
    12
      test/tags.js

+ 5
- 5
app.js View File

@@ -8,11 +8,11 @@ const express = require('express'),
8 8
     expressValidator = require('express-validator');
9 9
 
10 10
 // load internal dependencies
11
-let models = require('./models'),
12
-    config = require('./config'),
13
-    authenticate = require('./controllers/authenticate'),
14
-    deserialize = require('./controllers/deserialize'),
15
-    customValidators = require('./controllers/validators/custom');
11
+const models = require('./models'),
12
+      config = require('./config'),
13
+      authenticate = require('./controllers/authenticate'),
14
+      deserialize = require('./controllers/deserialize'),
15
+      customValidators = require('./controllers/validators/custom');
16 16
 
17 17
 
18 18
 // configure the database for all the models

+ 9
- 1
bin/www View File

@@ -8,6 +8,8 @@ var app = require('../app');
8 8
 var debug = require('debug')('ditapi:server');
9 9
 var http = require('http');
10 10
 
11
+const jobs = require('../jobs');
12
+
11 13
 /**
12 14
  * Get port from environment and store in Express.
13 15
  */
@@ -25,10 +27,16 @@ var server = http.createServer(app);
25 27
  * Listen on provided port, on all network interfaces.
26 28
  */
27 29
 
28
-server.listen(port);
30
+server.listen(port, function () {
31
+  jobs.start();
32
+});
29 33
 server.on('error', onError);
30 34
 server.on('listening', onListening);
31 35
 
36
+server.on('close', () => {
37
+  jobs.stop();
38
+});
39
+
32 40
 /**
33 41
  * Normalize a port into a number, string, or false.
34 42
  */

+ 23
- 0
jobs/index.js View File

@@ -0,0 +1,23 @@
1
+'use strict';
2
+
3
+/*
4
+ * Here we define regular jobs
5
+ * i.e. to keep the system clean
6
+ *
7
+ */
8
+
9
+const cron = require('node-cron'),
10
+      _ = require('lodash'),
11
+      tags = require('./tags');
12
+
13
+const tasks = [];
14
+
15
+// start all the tasks
16
+exports.start = function () {
17
+  // every day at 4 am delete all abandoned tags
18
+  tasks.push(cron.schedule('0 0 4 * * *', tags.deleteAbandoned));
19
+};
20
+
21
+exports.stop = function () {
22
+  _.each(tasks, task => { task.destroy(); });
23
+};

+ 11
- 0
jobs/tags.js View File

@@ -0,0 +1,11 @@
1
+'use strict';
2
+
3
+const path = require('path'),
4
+      models = require(path.resolve('./models'));
5
+
6
+async function deleteAbandonedTags() {
7
+  // returns a promise of a list of deleted tags
8
+  return await models.tag.deleteAbandoned();
9
+}
10
+
11
+exports.deleteAbandoned = deleteAbandonedTags;

+ 5
- 5
models/index.js View File

@@ -1,12 +1,12 @@
1 1
 'use strict';
2 2
 
3
-let model = require('./model'),
4
-    tag = require('./tag'),
5
-    user = require('./user'),
6
-    userTag = require('./user-tag');
3
+const model = require('./model'),
4
+      tag = require('./tag'),
5
+      user = require('./user'),
6
+      userTag = require('./user-tag');
7 7
 
8 8
 
9
-let models = {
9
+const models = {
10 10
   connect: function (params) {
11 11
     model.connect(params);
12 12
   },

+ 79
- 42
models/tag/index.js View File

@@ -1,19 +1,25 @@
1
-let path = require('path'),
2
-    _ = require('lodash'),
3
-    co = require('co');
1
+const path = require('path'),
2
+      _ = require('lodash');
4 3
 
5
-let Model = require(path.resolve('./models/model')),
6
-    schema = require('./schema');
4
+const Model = require(path.resolve('./models/model')),
5
+      schema = require('./schema');
7 6
 
8 7
 class Tag extends Model {
8
+
9
+  /**
10
+   * create a new tag
11
+   *
12
+   */
9 13
   static async create({ tagname, description, creator }) {
10
-    let tag = schema({ tagname, description });
11
-    let query = 'INSERT @tag IN tags';
12
-    let params = { tag };
14
+    // create the tag
15
+    const tag = schema({ tagname, description });
16
+    const query = 'INSERT @tag IN tags';
17
+    const params = { tag };
13 18
 
14 19
     await this.db.query(query, params);
15 20
 
16
-    let queryCreator = `
21
+    // create link to tag creator
22
+    const queryCreator = `
17 23
       FOR u IN users FILTER u.username == @creator
18 24
         FOR t IN tags FILTER t.tagname == @tagname
19 25
           INSERT {
@@ -22,7 +28,7 @@ class Tag extends Model {
22 28
             created: @created
23 29
           } IN tagCreator
24 30
           RETURN NEW`;
25
-    let paramsCreator = {
31
+    const paramsCreator = {
26 32
       creator,
27 33
       tagname,
28 34
       created: Date.now()
@@ -32,25 +38,25 @@ class Tag extends Model {
32 38
   }
33 39
 
34 40
   static async read(tagname) {
35
-    let query = `
41
+    const query = `
36 42
       FOR t IN tags FILTER t.tagname == @tagname
37 43
         FOR v, e, p
38 44
           IN 0..1
39 45
           OUTBOUND t
40 46
           OUTBOUND tagCreator
41 47
           RETURN KEEP(v, 'username', 'tagname', 'description', 'created')`;
42
-    let params = { tagname: tagname };
43
-    let out = await (await this.db.query(query, params)).all();
48
+    const params = { tagname };
49
+    const out = await (await this.db.query(query, params)).all();
44 50
 
45
-    let tag = _.pick(out[0], 'tagname', 'description', 'created');
46
-    let creator = _.pick(out[1], 'username');
51
+    const tag = _.pick(out[0], 'tagname', 'description', 'created');
52
+    const creator = _.pick(out[1], 'username');
47 53
     _.assign(tag, { creator });
48 54
 
49 55
     return (out[0]) ? tag : null;
50 56
   }
51 57
 
52 58
   static async update(tagname, { description, editor, time }) {
53
-    let query = `FOR t IN tags FILTER t.tagname == @tagname
59
+    const query = `FOR t IN tags FILTER t.tagname == @tagname
54 60
       UPDATE t WITH {
55 61
         description: @description,
56 62
         history: PUSH(t.history, {
@@ -61,50 +67,81 @@ class Tag extends Model {
61 67
       }
62 68
       IN tags
63 69
       RETURN NEW`;
64
-    let params = { tagname, description, editor, time };
65
-    let cursor = await this.db.query(query, params);
66
-    let output = await cursor.all();
70
+    const params = { tagname, description, editor, time };
71
+    const cursor = await this.db.query(query, params);
72
+    const output = await cursor.all();
67 73
     return output[0];
68 74
   }
69 75
 
70
-  static exists(tagname) {
71
-    return co.call(this, function * () {
72
-      let query = `
73
-        FOR t IN tags FILTER t.tagname == @tagname
74
-          COLLECT WITH COUNT INTO length
75
-          RETURN length`;
76
-      let params = { tagname: tagname };
77
-      let count = yield (yield this.db.query(query, params)).next();
78
-
79
-      switch (count) {
80
-        case 0:
81
-          return false;
82
-        case 1:
83
-          return true;
84
-        default:
85
-          throw new Error('bad output');
86
-      }
87
-    });
76
+  static async exists(tagname) {
77
+    const query = `
78
+      FOR t IN tags FILTER t.tagname == @tagname
79
+        COLLECT WITH COUNT INTO length
80
+        RETURN length`;
81
+    const params = { tagname };
82
+    const count = await (await this.db.query(query, params)).next();
83
+
84
+    switch (count) {
85
+      case 0:
86
+        return false;
87
+      case 1:
88
+        return true;
89
+      default:
90
+        throw new Error('bad output');
91
+    }
88 92
   }
89 93
 
90 94
   // get tags which start with likeTagname string
91 95
   static async filter(likeTagname) {
92
-    let query = `
96
+    const query = `
93 97
       FOR t IN tags FILTER t.tagname LIKE @likeTagname
94 98
         RETURN KEEP(t, 'username', 'tagname', 'description', 'created')`;
95 99
     // % serves as a placeholder for multiple characters in arangodb LIKE
96 100
     // _ serves as a placeholder for a single character
97
-    let params = { likeTagname: `%${likeTagname}%` };
98
-    let out = await (await this.db.query(query, params)).all();
101
+    const params = { likeTagname: `%${likeTagname}%` };
102
+    const out = await (await this.db.query(query, params)).all();
99 103
 
100
-    let formatted = [];
104
+    const formatted = [];
101 105
 
102
-    for(let tag of out) {
106
+    for(const tag of out) {
103 107
       formatted.push(_.pick(tag, ['tagname', 'description']));
104 108
     }
105 109
 
106 110
     return formatted;
107 111
   }
112
+
113
+  /**
114
+   * delete all tags which have no userTag edges
115
+   *
116
+   *
117
+   */
118
+  static async deleteAbandoned() {
119
+    const query = `
120
+      FOR t IN tags
121
+        LET e=(FOR e IN userTag
122
+            FILTER e._to == t._id
123
+            RETURN e)
124
+        FILTER LENGTH(e) < 1
125
+        REMOVE t IN tags RETURN KEEP(OLD, 'tagname')`;
126
+    // % serves as a placeholder for multiple characters in arangodb LIKE
127
+    // _ serves as a placeholder for a single character
128
+    const out = await (await this.db.query(query)).all();
129
+
130
+    return out;
131
+  }
132
+
133
+  /**
134
+   * counts all tags
135
+   */
136
+  static async count() {
137
+    const query = `
138
+      FOR t IN tags
139
+        COLLECT WITH COUNT INTO length
140
+        RETURN length`;
141
+    const count = await (await this.db.query(query)).next();
142
+
143
+    return count;
144
+  }
108 145
 }
109 146
 
110 147
 

+ 1
- 0
package.json View File

@@ -24,6 +24,7 @@
24 24
     "identicon.js": "^2.1.0",
25 25
     "jsonapi-serializer": "^3.4.1",
26 26
     "lodash": "^4.16.4",
27
+    "node-cron": "^1.1.3",
27 28
     "node-sass-middleware": "0.8.0",
28 29
     "nodemailer": "^2.6.4",
29 30
     "passport": "^0.3.2",

+ 45
- 12
test/tags.js View File

@@ -1,18 +1,19 @@
1 1
 'use strict';
2 2
 
3
-let supertest = require('supertest'),
4
-    should = require('should'),
5
-    path = require('path'),
6
-    _ = require('lodash');
3
+const supertest = require('supertest'),
4
+      should = require('should'),
5
+      path = require('path'),
6
+      _ = require('lodash');
7 7
 
8
-let app = require(path.resolve('./app')),
9
-    serializers = require(path.resolve('./serializers')),
10
-    models = require(path.resolve('./models')),
11
-    dbHandle = require(path.resolve('./test/handleDatabase'));
8
+const app = require(path.resolve('./app')),
9
+      serializers = require(path.resolve('./serializers')),
10
+      models = require(path.resolve('./models')),
11
+      tagJobs = require(path.resolve('./jobs/tags')),
12
+      dbHandle = require(path.resolve('./test/handleDatabase'));
12 13
 
13
-let serialize = serializers.serialize;
14
+const serialize = serializers.serialize;
14 15
 
15
-let agent = supertest.agent(app);
16
+const agent = supertest.agent(app);
16 17
 
17 18
 let dbData,
18 19
     loggedUser;
@@ -261,7 +262,39 @@ describe('/tags/:tagname', function () {
261 262
     });
262 263
   });
263 264
 
264
-  describe('DELETE', function () {
265
-    it('should delete the tag (when sufficient rights)');
265
+});
266
+
267
+describe('Deleting unused tags.', function () {
268
+
269
+  beforeEach(async function () {
270
+    const data = {
271
+      users: 1, // how many users to make
272
+      verifiedUsers: [0], // which  users to make verified
273
+      tags: 5,
274
+      userTag: [
275
+        [0, 1],
276
+        [0, 2]
277
+      ]
278
+    };
279
+
280
+    // create data in database
281
+    dbData = await dbHandle.fill(data);
282
+  });
283
+
284
+  // clear database after every test
285
+  afterEach(async function () {
286
+    await dbHandle.clear();
287
+  });
288
+
289
+  it('Unused tags should be deleted regularly with a cron-like job.', async function () {
290
+    // before we should have 5 tags
291
+    const countBefore = await models.tag.count();
292
+    should(countBefore).equal(5);
293
+
294
+    await tagJobs.deleteAbandoned();
295
+
296
+    // after running the job function we should have 2 tags left
297
+    const countAfter = await models.tag.count();
298
+    should(countAfter).equal(2);
266 299
   });
267 300
 });