diff --git a/api/interfaces/article-interface.ts b/api/interfaces/article-interface.ts index a42c209..9df65a6 100644 --- a/api/interfaces/article-interface.ts +++ b/api/interfaces/article-interface.ts @@ -1,4 +1,7 @@ +import { IUserModel } from '../models/user-model'; + + export interface IArticle { slug: string; title: string; @@ -9,4 +12,5 @@ export interface IArticle { updatedAt: Date; favorited: boolean; favoritesCount: number; + author: IUserModel; } diff --git a/api/interfaces/user-interface.ts b/api/interfaces/user-interface.ts index baf3119..5d53257 100644 --- a/api/interfaces/user-interface.ts +++ b/api/interfaces/user-interface.ts @@ -5,3 +5,11 @@ export interface IUser { bio?: string; image?: string; } + + +export interface IProfile { + username: string; + bio: string; + image: string; + following: boolean; +} diff --git a/api/models/article-model.ts b/api/models/article-model.ts index 74a1698..701441d 100644 --- a/api/models/article-model.ts +++ b/api/models/article-model.ts @@ -1,13 +1,11 @@ import { model, Model, Schema, Document } from 'mongoose'; import { IArticle } from '../interfaces/article-interface'; -import { IUserModel } from './user-model'; -// import * as mongoose from 'mongoose'; -// const User = mongoose.model('User'); +import { IUserModel, User } from './user-model'; export interface IArticleModel extends IArticle, Document { - toArticleJSON(user); + formatAsArticleJSON(user); } @@ -22,21 +20,22 @@ const ArticleSchema = new Schema({ }, {timestamps: true}); -ArticleSchema.methods.toArticleJSON = function(user: IUserModel) { +ArticleSchema.methods.formatAsArticleJSON = function(user: IUserModel) { + return { slug: this.slug, title: this.title, description: this.description, body: this.body, - tagList: this.tagList, createdAt: this.createdAt, updatedAt: this.updatedAt, + tagList: this.tagList, favorited: user ? user.isFavorite(this._id) : false, favoritesCount: this.favoritesCount, - author: this.author + author: this.author.formatAsProfileJSON(user) }; + }; -// TODO: taglist -// TODO fix author + export const Article: Model = model('Article', ArticleSchema); diff --git a/api/models/user-model.ts b/api/models/user-model.ts index e4062bc..e96e5f9 100644 --- a/api/models/user-model.ts +++ b/api/models/user-model.ts @@ -2,12 +2,12 @@ import { Document, Schema, Model, model } from 'mongoose'; import { IUser } from '../interfaces/user-interface'; import * as jwt from 'jsonwebtoken'; -import { jwtSecret } from './authentication'; +import { jwtSecret } from '../utilities/authentication'; import * as crypto from 'crypto'; export interface IUserModel extends IUser, Document { - token: string; + token?: string; generateJWT(); formatAsUserJSON(); setPassword(password); diff --git a/api/routes/articles-routes.ts b/api/routes/articles-routes.ts index b97ad90..8d98d74 100644 --- a/api/routes/articles-routes.ts +++ b/api/routes/articles-routes.ts @@ -1,46 +1,182 @@ -import { Router, Response, NextFunction } from 'express'; -import { authentication } from '../models/authentication'; -import { JWTRequest } from '../interfaces/requests-interface'; +import { Router, NextFunction, Response } from 'express'; +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'; const router: Router = Router(); +const Promise = require('bluebird'); /** - * // GET /api/articles + * GET /api/articles */ -router.get('/', authentication.optional, (req: JWTRequest, res: Response, next: NextFunction) => { +router.get('/', authentication.optional, (req: ProfileRequest, res: Response, next: NextFunction) => { - Article - .findOne() - .then( (article: IArticleModel) => { - // res.status(200).json({article: article.toArticleJSON()}); - res.json({message: 'working on this...'}); - } - ) - .catch(next); + // 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; - // .findById(req.payload.id) - // .then((user: IUserModel) => { - // res.status(200).json({user: user.formatAsUserJSON()}); - // } - // ) - // .catch(next); - // + + // Try to determine the user making the request + 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; } -); -// TODO: Remining routes + + // Define promises + const p1 = thisUser; + + const p2 = Article.count( {}); + + 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]; + + 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 // POST /api/articles // PUT /api/articles/:slug // DELETE /api/articles/:slug + // POST /api/articles/:slug/comments // GET /api/articles/:slug/comments // DELETE /api/articles/:slug/comments/:id + // POST /api/articles/:slug/favorite // DELETE /api/articles/:slug/favorite diff --git a/api/routes/profiles-routes.ts b/api/routes/profiles-routes.ts index 50f6a54..ba4b7f4 100644 --- a/api/routes/profiles-routes.ts +++ b/api/routes/profiles-routes.ts @@ -1,7 +1,7 @@ -import { NextFunction, Request, Response, Router } from 'express'; +import { NextFunction, Response, Router } from 'express'; import { IUserModel, User } from '../models/user-model'; -import { authentication } from '../models/authentication'; +import { authentication } from '../utilities/authentication'; import { ProfileRequest } from '../interfaces/requests-interface'; const router: Router = Router(); @@ -28,7 +28,7 @@ router.param('username', (req: ProfileRequest, res: Response, next: NextFunction */ router.get('/:username', authentication.optional, (req: ProfileRequest, res: Response, next: NextFunction) => { - // If authentication was performed and successful look up the profile relative to authenticated user + // If authentication was performed and was successful look up the profile relative to authenticated user if (req.payload) { User .findById(req.payload.id) @@ -39,15 +39,9 @@ router.get('/:username', authentication.optional, (req: ProfileRequest, res: Res // If authentication was NOT performed or successful look up profile relative to that same user (following = false) } else { - // User - // .findOne({username: req.params.username}) - // .then( (user: IUserModel) => { - // res.status(200).json({profile: user.formatAsProfileJSON(user)}); - // }) - // .catch(next); res.status(200).json({profile: req.profile.formatAsProfileJSON(req.profile)}); } - // ISSUE: why have to repeat query? Why route-level variable not accessible here? + }); @@ -87,7 +81,6 @@ router.delete('/:username/follow', authentication.required, (req: ProfileRequest }) .catch(next); }); -// TODO: DELETE route working on first try export const ProfilesRoutes: Router = router; diff --git a/api/routes/user-routes.ts b/api/routes/user-routes.ts index 59cf824..ae7cebf 100644 --- a/api/routes/user-routes.ts +++ b/api/routes/user-routes.ts @@ -1,6 +1,6 @@ import { IUserModel, User } from '../models/user-model'; -import { authentication } from '../models/authentication'; +import { authentication } from '../utilities/authentication'; import { NextFunction, Response, Router } from 'express'; import { JWTRequest } from '../interfaces/requests-interface'; diff --git a/api/models/authentication.ts b/api/utilities/authentication.ts similarity index 100% rename from api/models/authentication.ts rename to api/utilities/authentication.ts diff --git a/api/utilities/passport.ts b/api/utilities/passport.ts index 6c9b7ca..9c6b87d 100644 --- a/api/utilities/passport.ts +++ b/api/utilities/passport.ts @@ -3,6 +3,7 @@ import * as passport from 'passport'; import { User } from '../models/user-model'; import * as passportLocal from 'passport-local'; + const LocalStrategy = passportLocal.Strategy; diff --git a/package-lock.json b/package-lock.json index d6f8728..7da575b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,15 @@ "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", diff --git a/package.json b/package.json index 8501015..15db1f1 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "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",