Skip to content
This repository has been archived by the owner on Jan 20, 2020. It is now read-only.

Commit

Permalink
Merge pull request #20 from lucianonooijen/feature/articles
Browse files Browse the repository at this point in the history
Feature/articles (can be merged)
  • Loading branch information
Luciano Nooijen authored Dec 6, 2018
2 parents d55c2a5 + e91965e commit 4ddf7f0
Show file tree
Hide file tree
Showing 24 changed files with 896 additions and 132 deletions.
13 changes: 5 additions & 8 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,16 @@
"type": "node",
"name": "Launch Jest Tests",
"request": "launch",
"env": {"NODE_ENV": "test"},
"args": [
"--runInBand"
"--runInBand",
"-i"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
},
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/server.js"
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
"runtimeExecutable": "/usr/local/bin/node"
},
]
}
78 changes: 61 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
[![NodeJS Version](https://img.shields.io/badge/Node%20Version-%3E%3D%20v8.0.0-green.svg)](https://img.shields.io/badge/Node%20Version-%3E%3D%20v8.0.0-green.svg)
[![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](http://perso.crans.org/besson/LICENSE.html)

**NOTE: THIS PACKAGE IS STILL IN DEVELOPMENT! RELEASE OF v1.0.0 WILL BE AROUND DECEMBER 1st, 2018.**

Blog API developed by Bytecode Digital Agency as free (as in freedom) open source software. Built in NodeJS. Available as a standalone server, or as a NPM package

## Installation
Expand All @@ -35,36 +33,82 @@ For contributing to the development, fork the [GitHub repository](https://github
To use the NodeJS Blog module, first, import the package

```js
const NodeBlog = require('nodejs-blog');
const nodeBlog = require('nodejs-blog');
const { authors, auth, users, categories, articles } = require('nodejs-blog');
```

or using ES6 modules
to start using the package, create a new instance of the NodeBlog class

```js
import NodeBlog from 'nodejs-blog';
const client = 'YOUR_DB_CLIENT'; // for more info, see https://knexjs.org/
const host = 'YOUR_DB_HOST';
const database = 'YOUR_DB_NAME';
const user = 'YOUR_DB_USER';
const pass = 'YOUR_DB_PASS';
const debug = true || false;

const blog = nodeBlog(client, host, user, database, password, debug);
```

to start using the package, create a new instance of the NodeBlog class
For authentication you should set the following environment (`process.env.[variable] = value`) variables, or the auth methods will not work:
```
SALT_ROUNDS=number
JWT_SECRET=string
JWT_EXPIRES_IN_DAYS=number_of_days
```

Then you can use the imported functions as you wish, for example:

```js
const nodeBlogConfig = {
client: 'YOUR_DB_CLIENT', // for more info, see https://knexjs.org/
host: 'YOUR_DB_HOST',
database: 'YOUR_DB_NAME',
user: 'YOUR_DB_USER',
pass: 'YOUR_DB_PASS',
debug: true || false,
};
const nodeBlog = new NodeBlog(nodeBlogConfig);
const posts = await articles.list(blog);
```

Just send the `blog` instance as the first argument and the rest of the arguments second. This is because this way the same logic can be applied to multiple blog instances within an application.

The available methods are:

```js
authors.list(blog)
authors.get(blog, id)
authors.add(blog, authorObject)
authors.modify(blog, id, modifiedData)
authors.delete(blog, id)

auth.authenticate(blog, username, password) // Returns true/false
auth.generateToken(blog, username, password) // Returns JWT, throws error if invalid credentials
auth.decode(jwt) // Returns decoded object
auth.validate(blog, username, password) // Returns true/false

users.list(blog)
users.get(blog, id)
users.add(blog, userObject)
users.modify(blog, id, modifiedData)
users.delete(blog, id)

categories.list(blog)
categories.get(blog, id)
categories.add(blog, categoryObject)
categories.modify(blog, id, modifiedData)
categories.delete(blog, id)

articles.list(blog)
articles.get(blog, id)
articles.add(blog, articleObject)
// articles.modify(blog, id, modifiedData) // not available yet
articles.delete(blog, id)
```

We recommend creating a single file that will create the NodeBlog instance, and `export` this instance, and `import` in all other files where you want to use NodeJS Blog.

For security reasons we recommend using environment variables for loading the configuration. This is also in compliance with the [12 factor app Config guidelines](https://12factor.net/config)

Note: NodeJS blog was made to be used with PostgreSQL, but it should(/could) also be compatible with other databases, as it uses KnexJS under the hood.
Note: NodeJS blog was made to be used with PostgreSQL, but it should(/could) also be compatible with other databases, as it uses [KnexJS](https://knexjs.org) under the hood.

*A demo application is currently in development*

## Running the API as a standalone service (still in development, might not work 100%)

## Running the API as a standalone service
First clone the repository and `cd` into the directory.

To run NodeJS Blog as a standalone service, run `cp .env.example .env` to create the `.env` file.

Expand Down
205 changes: 205 additions & 0 deletions controllers/articles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/* eslint max-len: ["error", { "code": 100 }] */
/* eslint-disable prettier/prettier */

// const { knex } = require('../helpers');

const fieldsBase = [
'articles.id',
'articles.title',
'articles.subtitle',
'articles.slug',
'articles.posted_on',
'article_content.image_url AS article_image_url',
'article_content.summary',
'authors.name AS author_name',
'authors.role AS author_role',
'authors.image_url AS author_image_url',
'categories.name AS category_name',
'categories.slug AS category_slug',
];

const calculateReadingTime = text => {
try {
const wordsPerMinute = 275;
const wordArr = text.split(' ');
const textWordAmount = wordArr.length;
const readingTimeInMinutes = Math.floor(textWordAmount / wordsPerMinute);
return readingTimeInMinutes;
} catch (err) {
return null;
}
};

const addReadingTimeToArticles = articles => {
const articlesWithReadingTime = articles.map(article => {
const articleContent = article.html_content;
const readingTime = calculateReadingTime(articleContent);
const readingTimeObject = { reading_time: readingTime };
const updatedArticle = Object.assign({}, article, readingTimeObject);
return updatedArticle;
});
return articlesWithReadingTime;
};

const listArticles = async knex => {
const fields = [
...fieldsBase,
'article_content.html_content',
];
const articles = await knex
.select(fields)
.from('articles')
.join('article_content', 'article_content.article_id', '=', 'articles.id')
.join('categories', 'categories.id', '=', 'articles.category')
.join('authors', 'authors.id', '=', 'articles.author')
.where('articles.hidden', '=', false)
.andWhere('articles.posted_on', '<=', knex.raw('now()'));
const articlesWithReadingTime = addReadingTimeToArticles(articles);
return articlesWithReadingTime;
};

const getRelatedArticles = async (knex, id) => {
const fields = [
...fieldsBase,
'article_content.html_content',
];
const relatedArticles = await knex
.select(fields)
.from('articles')
.join('article_content', 'article_content.article_id', '=', 'articles.id')
.join('categories', 'categories.id', '=', 'articles.category')
.join('authors', 'authors.id', '=', 'articles.author')
.join('related_articles', 'related_articles.related_article_id', '=', 'articles.id')
.where('related_articles.article_id', '=', id)
.andWhere('articles.hidden', '=', false)
.andWhere('articles.posted_on', '<=', knex.raw('now()'));
const articlesWithReadingTime = addReadingTimeToArticles(relatedArticles);
return articlesWithReadingTime;
};

const addRelatedArticlesToArticleObject = async (knex, id, article) => {
const relatedArticles = await getRelatedArticles(knex, id);
if (relatedArticles.length === 0) {
return article;
}
const articleWithRelatedArticles = {
...article,
related_articles: relatedArticles,
};
return articleWithRelatedArticles;
};

const getArticle = async (knex, id) => {
const fields = [
...fieldsBase,
'article_content.html_content',
];
const articles = await knex
.select(fields)
.from('articles') // eslint-disable-next-line
.join('article_content', 'article_content.article_id', '=', 'articles.id')
.join('categories', 'categories.id', '=', 'articles.category')
.join('authors', 'authors.id', '=', 'articles.author')
.where('articles.id', '=', id);
const articlesWithReadingTime = addReadingTimeToArticles(articles);
const articleBase = articlesWithReadingTime[0];
const article = await addRelatedArticlesToArticleObject(knex, id, articleBase);
return article;
};

const addToArticlesTable = async (knex, articleData) => {
const returning = ['id', 'title', 'subtitle', 'posted_on', 'slug', 'author', 'category'];
const addedArticle = await knex('articles')
.insert([articleData])
.returning(returning);
return addedArticle[0];
};

const addToArticleContentTable = async (knex, articleData) => {
const returning = ['summary', 'image_url', 'html_content'];
const addedArticle = await knex('article_content')
.insert([articleData])
.returning(returning);
return addedArticle[0];
};

const generateRelatedArticles = (id, relatedArticles) => {
const relatedArticlesObjects = relatedArticles.map(relatedArticle => {
const relatedArticleObject = {
article_id: id,
related_article_id: relatedArticle,
};
return relatedArticleObject;
});
return relatedArticlesObjects;
};

const addToRelatedArticlesTable = async (knex, id, relatedArticles) => {
if (!relatedArticles || relatedArticles.length === 0) {
return [];
}
const relatedArticlesArray = generateRelatedArticles(id, relatedArticles);
const addedRelatedArticles = await knex('related_articles')
.insert(relatedArticlesArray)
.returning('article_id', 'related_article_id');
return addedRelatedArticles;
};

const addArticle = async (knex, article) => {
const articleData = {
title: article.title,
subtitle: article.subtitle,
posted_on: article.posted_on,
hidden: article.hidden,
slug: article.slug,
author: article.author,
category: article.author,
};
const addedArticleData = await addToArticlesTable(knex, articleData);
const addedArticleId = addedArticleData.id;
const articleContentData = {
article_id: addedArticleId,
summary: article.summary,
image_url: article.image_url,
html_content: article.html_content,
};
await addToArticleContentTable(knex, articleContentData);
const relatedArticleIds = article.related_articles;
await addToRelatedArticlesTable(knex, addedArticleId, relatedArticleIds);
const createdArticle = await getArticle(knex, addedArticleId);
return createdArticle;
};

// TODO: fix and test better
// eslint-disable-next-line
const modifyArticle = async (knex, id, article) => {
throw new Error('Article modification is not supported');
};

const deleteArticle = async (knex, id) => {
await knex('related_articles')
.where({ article_id: id })
.orWhere({ related_article_id: id })
.delete();
await knex('article_content')
.where({ article_id: id })
.delete();
await knex('articles')
.where({ id })
.delete();
return { id };
};

module.exports = {
listArticles,
getRelatedArticles,
addRelatedArticlesToArticleObject,
calculateReadingTime,
addReadingTimeToArticles,
addToRelatedArticlesTable,
getArticle,
generateRelatedArticles,
addArticle,
modifyArticle,
deleteArticle,
};
Loading

0 comments on commit 4ddf7f0

Please sign in to comment.