Browse Source

GET /ideas?filter[creators]='username0,username1,username2' (#54)

* GET /ideas?filter[creators]=username0,username1,username2

* fix bugs GET /ideas?filter[creators]=username0,username1,username2

* correct code after review GET /ideas?filter[creators]=username0,username1,username2
Agata Andrzejewska 2 years ago
parent
commit
ce64dc626d

+ 1
- 0
apidoc.raml View File

@@ -371,6 +371,7 @@ types:
371 371
       - ideas with my tags: `?filter[withMyTags]`
372 372
       - ideas with provided tags: `?filter[withTags]=tagname0,tagname1,tagname2`
373 373
       - new ideas: `?sort=-created`
374
+      - ideas with provided creators: `?filter[creators]=username0,username1,username2`
374 375
   /{id}:
375 376
     get:
376 377
       description: Read an idea by id.

+ 2
- 1
controllers/goto/ideas.js View File

@@ -7,6 +7,7 @@ module.exports = {
7 7
     withMyTags: route(['query.filter.withMyTags']),
8 8
     withTags: route(['query.filter.withTags']),
9 9
     new: route(['query.sort'], 'newQuery'),
10
-    random: route(['query.filter.random'])
10
+    random: route(['query.filter.random']),
11
+    withCreators: route(['query.filter.creators'])
11 12
   },
12 13
 };

+ 24
- 1
controllers/ideas.js View File

@@ -185,4 +185,27 @@ async function getRandomIdeas(req, res, next) {
185 185
   }
186 186
 }
187 187
 
188
-module.exports = { get, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patch, post };
188
+/**
189
+ * Get ideas with specified creators
190
+ */
191
+async function getIdeasWithCreators(req, res, next) {
192
+  try {
193
+    // gather data
194
+    const { page: { offset = 0, limit = 10 } = { } } = req.query;
195
+    const { creators } = req.query.filter;
196
+
197
+    // read ideas from database
198
+    const foundIdeas = await models.idea.findWithCreators(creators, { offset, limit });
199
+
200
+    // serialize
201
+    const serializedIdeas = serialize.idea(foundIdeas);
202
+
203
+    // respond
204
+    return res.status(200).json(serializedIdeas);
205
+
206
+  } catch (e) {
207
+    return next(e);
208
+  }
209
+}
210
+
211
+module.exports = { get, getIdeasWithCreators, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patch, post };

+ 1
- 0
controllers/validators/ideas.js View File

@@ -4,6 +4,7 @@ const validate = require('./validate-by-schema');
4 4
 
5 5
 module.exports = {
6 6
   get: validate('getIdea'),
7
+  getIdeasWithCreators: validate('getIdeasWithCreators'),
7 8
   getIdeasWithMyTags: validate('getIdeasWithMyTags'),
8 9
   getIdeasWithTags: validate('getIdeasWithTags'),
9 10
   getNewIdeas: validate('getNewIdeas'),

+ 2
- 1
controllers/validators/parser.js View File

@@ -25,7 +25,8 @@ const parametersDictionary = {
25 25
     withMyTags: 'int',
26 26
     location: 'coordinates',
27 27
     relatedToTags: 'array',
28
-    size: 'int'
28
+    size: 'int',
29
+    creators: 'array'
29 30
   },
30 31
 };
31 32
 

+ 7
- 1
controllers/validators/schema/definitions.js View File

@@ -1,6 +1,6 @@
1 1
 'use strict';
2 2
 
3
-const { tagname } = require('./paths');
3
+const { tagname, username } = require('./paths');
4 4
 
5 5
 module.exports = {
6 6
   shared: {
@@ -168,6 +168,12 @@ module.exports = {
168 168
       items: tagname,
169 169
       maxItems: 10,
170 170
       minItems: 1
171
+    },
172
+    usersList: {
173
+      type: 'array',
174
+      items: username,
175
+      maxItems: 10,
176
+      minItems: 1
171 177
     }
172 178
   }
173 179
 };

+ 22
- 2
controllers/validators/schema/ideas.js View File

@@ -1,6 +1,6 @@
1 1
 'use strict';
2 2
 
3
-const { title, detail, id, page, pageOffset0, random, tagsList } = require('./paths');
3
+const { title, detail, id, page, pageOffset0, random, tagsList, usersList } = require('./paths');
4 4
 
5 5
 const postIdeas = {
6 6
   properties: {
@@ -133,4 +133,24 @@ const getRandomIdeas = {
133 133
   required: ['query']
134 134
 };
135 135
 
136
-module.exports = { getIdea, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patchIdea, postIdeas };
136
+const getIdeasWithCreators = {
137
+  properties: {
138
+    query: {
139
+      properties: {
140
+        filter: {
141
+          properties: {
142
+            creators: usersList
143
+          },
144
+          required: ['creators'],
145
+          additionalProperties: false
146
+        },
147
+        page
148
+      },
149
+      required: ['filter'],
150
+      additionalProperties: false
151
+    },
152
+  },
153
+  required: ['query']
154
+};
155
+
156
+module.exports = { getIdea, getIdeasWithCreators, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patchIdea, postIdeas };

+ 1
- 0
controllers/validators/schema/paths.js View File

@@ -20,6 +20,7 @@ module.exports = {
20 20
   pageOffset0: { $ref: 'sch#/definitions/query/page0' }, // page with offset = 0
21 21
   random: { $ref: 'sch#/definitions/query/random' },
22 22
   tagsList: { $ref: 'sch#/definitions/query/tagsList' },
23
+  usersList: { $ref: 'sch#/definitions/query/usersList' },
23 24
   ideaId: { $ref : 'sch#/definitions/idea/ideaId' },
24 25
   title: { $ref: 'sch#/definitions/idea/titl' },
25 26
   detail: { $ref: 'sch#/definitions/idea/detail' },

+ 29
- 0
models/idea/index.js View File

@@ -216,6 +216,35 @@ class Idea extends Model {
216 216
     const cursor = await this.db.query(query, params);
217 217
     return await cursor.all();
218 218
   }
219
+
220
+  /**
221
+   * Read ideas with specified creators
222
+   * @param {string[]} usernames - list of usernames to search with
223
+   * @param {integer} offset - pagination offset
224
+   * @param {integer} limit - pagination limit
225
+   * @returns {Promise<Idea[]>} - list of found ideas
226
+   */
227
+  static async findWithCreators(creators, { offset, limit }) {
228
+    // TODO  to be checked for query optimization or unnecessary things
229
+    const query = `
230
+      LET creators = (FOR u IN users FILTER u.username IN @creators RETURN u)
231
+        FOR idea IN ideas FILTER idea.creator IN creators[*]._id
232
+            // find creator
233
+            LET c = (DOCUMENT(idea.creator))
234
+            // format for output
235
+            LET creator = MERGE(KEEP(c, 'username'), c.profile)
236
+            LET ideaOut = MERGE(KEEP(idea, 'title', 'detail', 'created'), { id: idea._key}, { creator })
237
+            // sort from newest
238
+            SORT idea.created DESC
239
+            // limit
240
+            LIMIT @offset, @limit
241
+            // respond
242
+            RETURN ideaOut`;
243
+
244
+    const params = { offset, limit , creators };
245
+    const cursor = await this.db.query(query, params);
246
+    return await cursor.all();
247
+  }
219 248
 }
220 249
 
221 250
 module.exports = Idea;

+ 1
- 0
package.json View File

@@ -7,6 +7,7 @@
7 7
     "start": "NODE_ENV=development node ./bin/www",
8 8
     "start:production": "NODE_ENV=production PORT=3001 node ./bin/www",
9 9
     "test": "NODE_ENV=test mocha ./test/",
10
+    "test:nyan": "NODE_ENV=test mocha -R nyan ./test/",
10 11
     "test:watch": "NODE_ENV=test mocha ./test/ --watch",
11 12
     "unit": "NODE_ENV=test mocha ./test/unit/ --recursive",
12 13
     "unit:watch": "NODE_ENV=test mocha ./test/unit/ --watch --recursive",

+ 4
- 0
routes/ideas.js View File

@@ -32,6 +32,10 @@ router.route('/')
32 32
 router.route('/')
33 33
   .get(go.get.random, authorize.onlyLogged, parse, ideaValidators.getRandomIdeas, ideaControllers.getRandomIdeas);
34 34
 
35
+// get ideas with creators
36
+router.route('/')
37
+  .get(go.get.withCreators, authorize.onlyLogged, parse, ideaValidators.getIdeasWithCreators, ideaControllers.getIdeasWithCreators);
38
+
35 39
 router.route('/:id')
36 40
   // read idea by id
37 41
   .get(authorize.onlyLogged, ideaValidators.get, ideaControllers.get)

+ 128
- 0
test/ideas.list.js View File

@@ -414,4 +414,132 @@ describe('read lists of ideas', () => {
414 414
       });
415 415
     });
416 416
   });
417
+
418
+  describe('GET /ideas?filter[creators]=user0,user1,user2', () => {
419
+    let user0,
420
+        user2,
421
+        user3,
422
+        user4;
423
+    // create and save testing data
424
+    beforeEach(async () => {
425
+      const data = {
426
+        users: 6,
427
+        tags: 6,
428
+        verifiedUsers: [0, 1, 2, 3, 4],
429
+        ideas: [[{}, 0], [{}, 0],[{}, 1],[{}, 2],[{}, 2],[{}, 2],[{}, 3]]
430
+      };
431
+
432
+      dbData = await dbHandle.fill(data);
433
+
434
+      [user0, , user2, user3, user4, ] = dbData.users;
435
+    });
436
+
437
+    context('logged in', () => {
438
+
439
+      beforeEach(() => {
440
+        agent = agentFactory.logged(user0);
441
+      });
442
+
443
+      context('valid data', () => {
444
+
445
+        it('[one creator] 200 and return array of matched ideas', async () => {
446
+
447
+          // request
448
+          const response = await agent
449
+            .get(`/ideas?filter[creators]=${user2.username}`)
450
+            .expect(200);
451
+
452
+          // we should find 2 ideas...
453
+          should(response.body).have.property('data').Array().length(3);
454
+
455
+          // sorted by creation date desc
456
+          should(response.body.data.map(idea => idea.attributes.title))
457
+            .eql([5, 4, 3].map(no => `idea title ${no}`));
458
+
459
+        });
460
+
461
+
462
+        it('[two creators] 200 and return array of matched ideas', async () => {
463
+
464
+          // request
465
+          const response = await agent
466
+            .get(`/ideas?filter[creators]=${user2.username},${user3.username}`)
467
+            .expect(200);
468
+
469
+          // we should find 5 ideas...
470
+          should(response.body).have.property('data').Array().length(4);
471
+
472
+          // sorted by creation date desc
473
+          should(response.body.data.map(idea => idea.attributes.title))
474
+            .eql([6, 5, 4, 3].map(no => `idea title ${no}`));
475
+        });
476
+
477
+        it('[creator without ideas] 200 and return array of matched ideas', async () => {
478
+
479
+          // request
480
+          const response = await agent
481
+            .get(`/ideas?filter[creators]=${user4.username}`)
482
+            .expect(200);
483
+
484
+          // we should find 0 ideas...
485
+          should(response.body).have.property('data').Array().length(0);
486
+
487
+        });
488
+
489
+        it('[pagination] offset and limit the results', async () => {
490
+          const response = await agent
491
+            .get(`/ideas?filter[creators]=${user2.username},${user3.username}&page[offset]=1&page[limit]=3`)
492
+            .expect(200);
493
+
494
+          // we should find 3 ideas
495
+          should(response.body).have.property('data').Array().length(3);
496
+
497
+          // sorted by creation date desc
498
+          should(response.body.data.map(idea => idea.attributes.title))
499
+            .eql([5, 4, 3].map(no => `idea title ${no}`));
500
+        });
501
+
502
+        it('[nonexistent creator] 200 and return array of matched ideas', async () => {
503
+
504
+          // request
505
+          const response = await agent
506
+            .get('/ideas?filter[creators]=nonexistentcreator')
507
+            .expect(200);
508
+
509
+          // we should find 0 ideas...
510
+          should(response.body).have.property('data').Array().length(0);
511
+
512
+        });
513
+      });
514
+
515
+      context('invalid data', () => {
516
+
517
+        it('[invalid query.filter.creators] 400', async () => {
518
+          await agent
519
+            .get('/ideas?filter[creators]=1')
520
+            .expect(400);
521
+        });
522
+
523
+        it('[invalid pagination] 400', async () => {
524
+          await agent
525
+            .get(`/ideas?filter[creators]=${user2.username},${user3.username}&page[offset]=1&page[limit]=21`)
526
+            .expect(400);
527
+        });
528
+
529
+        it('[unexpected query params] 400', async () => {
530
+          await agent
531
+            .get(`/ideas?filter[creators]=${user2.username},${user3.username}&additional[param]=3&page[offset]=1&page[limit]=3`)
532
+            .expect(400);
533
+        });
534
+      });
535
+    });
536
+
537
+    context('not logged in', () => {
538
+      it('403', async () => {
539
+        await agent
540
+          .get(`/ideas?filter[creators]=${user2.username}`)
541
+          .expect(403);
542
+      });
543
+    });
544
+  });
417 545
 });