Completed route: GET /api/articles

GET /api/articles route working with tags, including repeated tags where appropriate.

Ex: {{apiUrl}}/articles?offset=0&limit=3&tag=GIT&author=josh&favorited=johnjacob4&favorited=josh

Exposed user property favorites.

Cleaned up some unused packages.
This commit is contained in:
Josh Black 2017-09-21 10:43:03 -06:00
parent 0c3c909ae6
commit c2a449b2a9
6 changed files with 96 additions and 138 deletions

View File

@ -14,3 +14,10 @@ export interface IArticle {
favoritesCount: number;
author: IUserModel;
}
export interface IQuery {
tagList: {$in: any[]};
author: string;
_id: {$in: any[]};
}

View File

@ -10,7 +10,7 @@ export interface IArticleModel extends IArticle, Document {
const ArticleSchema = new Schema({
slug: {type: String, lowercase: true, unique: true},
slug: {type: String}, // FIXME: Add , lowercase: true, unique: true
title: String,
description: String,
body: String,

View File

@ -8,6 +8,7 @@ import * as crypto from 'crypto';
export interface IUserModel extends IUser, Document {
token?: string;
favorites: [Schema.Types.ObjectId];
generateJWT();
formatAsUserJSON();
setPassword(password);

View File

@ -4,10 +4,11 @@ import { authentication } from '../utilities/authentication';
import { ProfileRequest } from '../interfaces/requests-interface';
import { Article, IArticleModel } from '../models/article-model';
import { IUserModel, User } from '../models/user-model';
// import * as Promise from 'bluebird';
import { IQuery } from '../interfaces/article-interface';
import { Schema } from 'mongoose';
const router: Router = Router();
const Promise = require('bluebird');
const Promise = require('bluebird'); // FIXME: how to handle this in Typescript?
/**
@ -15,11 +16,6 @@ const Promise = require('bluebird');
*/
router.get('/', authentication.optional, (req: ProfileRequest, res: Response, next: NextFunction) => {
// Handle all URL query parameters
const limit: number = req.query.limit ? Number(req.query.limit) : 20;
const offset: number = req.query.offset ? Number(req.query.offset) : 0;
// Try to determine the user making the request
let thisUser: IUserModel;
@ -37,135 +33,99 @@ router.get('/', authentication.optional, (req: ProfileRequest, res: Response, ne
thisUser = req.profile;
}
// Parse URL query strings and create a query object
const limit: number = req.query.limit ? Number(req.query.limit) : 20;
const offset: number = req.query.offset ? Number(req.query.offset) : 0;
// Define promises
const p1 = thisUser;
const query = <IQuery> {}; // ISSUE: how to resolve?
const p2 = Article.count( {});
// Handle single tag or multiple tags in query (...&tag=git&tag=node...)
if (typeof req.query.tag !== 'undefined') {
if (Array.isArray(req.query.tag)) {
query.tagList = {$in: req.query.tag};
} else {
query.tagList = {$in: [req.query.tag]};
}
}
const p3 = Article.find().limit(limit).skip(offset).populate('author').catch();
// Resolve and use promise results
Promise
.all([p1, p2, p3])
.then(results => {
const user: IUserModel = results[0];
const articlesCount: number = results[1];
const articles = results[2];
.all([
req.query.author ? User.findOne({username: req.query.author}) : 'noAuthor',
req.query.favorited
? User
.find({username: req.query.favorited})
.then(users => {
let favoritedArticles: [Schema.Types.ObjectId];
users.forEach((user, userIndex) => {
user.favorites.forEach((favorite, favoriteIndex) => {
if (userIndex === 0 && favoriteIndex === 0) {
favoritedArticles = [favorite];
} else {
favoritedArticles.push(favorite);
}
});
});
return favoritedArticles;
})
: 'noFavorites'
])
.then( results => {
const author = results[0];
const favoritedArticleIds = results[1];
res.json(
{articles: articles.map((article: IArticleModel) => {
return article.formatAsArticleJSON(user);
}),
articlesCount});
})
.catch(next);
// Return no articles for unknown author, but ignore author filter if none was provided
if (author !== 'noAuthor') {
query.author = author;
}
/* Restrict the query results to only article IDs that are
favorited by the username(s) specified in the query string.
Note: Choosing to interpret multiple usernames as an 'or' operation,
meaning that articles favorited by ANY of the users will be returned,
as opposed to an 'and' operation wherein only articles favorited by
ALL usernames would be returned.
*/
if (favoritedArticleIds !== 'noFavorites') {
query._id = {$in: favoritedArticleIds};
}
// Define promises
const p1 = thisUser;
const p2 = Article.count(query).exec();
/* ISSUE: Should count be MIN(count, limit)? or should it count all results,
even if not displayed due to limit or offset query string parameter
*/
const p3 =
Article
.find(query)
.limit(limit)
.skip(offset) // FIXME: does order matter?
.populate('author')
.exec();
// Resolve and use promise results
Promise
.all([p1, p2, p3])
.then(results => {
const user: IUserModel = results[0];
const articlesCount: number = results[1];
const articles = results[2];
res.json(
{articles: articles.map((article: IArticleModel) => {
return article.formatAsArticleJSON(user);
}),
articlesCount});
})
.catch(next);
});
});
// PREVIOUS ATTEMPT:
// router.get('/', authentication.optional, (req: ProfileRequest, res: Response, next: NextFunction) => {
//
// let articlesCount = 0;
// let thisUser: IUserModel;
//
// // If authentication was performed was successful look up the profile relative to authenticated user
// if (req.payload) {
// User
// .findById(req.payload.id)
// .then( (user: IUserModel) => {
// return thisUser = req.profile.formatAsProfileJSON(user);
// })
// .catch();
//
// // If authentication was NOT performed or successful look up profile relative to that same user (following = false)
// } else {
// thisUser = req.profile;
// }
//
// Article
// .count( (err, count) => {
// articlesCount = count;
// })
// .find()
// .sort({updatedAt: 'desc'})
// .then( (articles: IArticleModel[]) => {
// res.json({articles: articles.map(article => {
// console.log(article);
// return article.formatAsArticleJSON(thisUser);
// }), articlesCount
// });
// })
// .catch(next);
// });
// WORKING:
// interface IQuery {
// tagList: {$in: any[]};
// author: string;
// _id: {$in: any[]};
// limit: number;
// offset: number;
// }
// let query: IQuery;
// let limit = 20;
// let offset = 0;
//
// if (typeof req.query.limit !== 'undefined') {
// limit = req.query.limit;
// }
//
// if (typeof req.query.offset !== 'undefined') {
// offset = req.query.offset;
// }
//
// if ( typeof req.query.tag !== 'undefined' ) {
// query.tagList = {$in : [req.query.tag]};
// }
//
// Promise.all([
// req.query.author ? User.findOne({username: req.query.author}) : null,
// req.query.favorited ? User.findOne({username: req.query.favorited}) : null
// ]).then(function(results){
// const author = results[0];
// const favoriter = results[1];
//
// if (author) {
// query.author = author._id;
// }
//
// if (favoriter) {
// query._id = {$in: favoriter.favorites};
// } else if (req.query.favorited) {
// query._id = {$in: []};
// }
//
// Promise.all([
// Article.find(query)
// .limit(Number(limit))
// .skip(Number(offset))
// .sort({createdAt: 'desc'})
// .populate('author')
// .exec(),
// Article.count(query).exec(),
// req.payload ? User.findById(req.payload.id) : null,
// ]).then(function(results){
// const articles = results[0];
// const articlesCount = results[1];
// const user = results[2];
//
// return res.json({
// articles: articles.map(function(article) {
// return article.formatAsArticleJSON(user);
// }),
// articlesCount
// });
// });
// }).catch(next);
// TODO: Remaining routes
// GET /api/articles/feed
// GET /api/articles/:slug

9
package-lock.json generated
View File

@ -10,15 +10,6 @@
"integrity": "sha512-rBfrD56OxaqVjghtVqp2EEX0ieHkRk6IefDVrQXIVGvlhDOEBTvZff4Q02uo84ukVkH4k5eB1cPKGDM2NlFL8A==",
"dev": true
},
"@types/bluebird-global": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/@types/bluebird-global/-/bluebird-global-3.5.3.tgz",
"integrity": "sha512-EP8wk7BRKtb9MhdHSc/xeUSfBByHQpm6pFwvQV1dO0d2o0wShYB9xEOx+OGP+2VNtADaBnhvXJ+Z5sgseR+f/w==",
"dev": true,
"requires": {
"@types/bluebird": "3.5.8"
}
},
"@types/bson": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.4.tgz",

View File

@ -19,7 +19,6 @@
"homepage": "https://",
"devDependencies": {
"@types/bluebird": "^3.5.8",
"@types/bluebird-global": "^3.5.3",
"@types/express": "^4.0.37",
"@types/express-jwt": "0.0.37",
"@types/express-session": "^1.15.3",