Skip to content

Commit

Permalink
Adds Initial Service to Get Posts (#1838)
Browse files Browse the repository at this point in the history
* Initial Posts Service Draft

* Updated redis container name, removed second ports, and updated redis url

* Added initial code to test /posts endpoint

* Refactored and added jest config settings for test cases

* Recovered and fixed lost files issue

* Finished tests for posts service

* Revert "Finished tests for posts service"

This reverts commit abfc7f5.

* Revert "Added initial code to test /posts endpoint"

This reverts commit a0b9ad2.

* Added install script for posts service

* Updated tests, package.json, fixed Docker typo

* Added missing - to Dockerfile

* Removed unused files, and updated docker, and env file

* Updated Dockerfile, jest, and added back supertest

* Added post service env info to staging and production env files
  • Loading branch information
HyperTHD committed Mar 12, 2021
1 parent 2e6fe53 commit e4904a5
Show file tree
Hide file tree
Showing 20 changed files with 789 additions and 1 deletion.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -12,6 +12,7 @@
"api:stop": "docker-compose -f ./src/api/docker-compose-api.yml down",
"install:auth-service": "cd src/api/auth && npm install",
"install:image-service": "cd src/api/image && npm install",
"install:posts-service": "cd src/api/posts && npm install",
"install:autodeployment": "cd tools/autodeployment && npm install",
"install:next": "cd src/web && npm install",
"build": "npm run build --prefix src/web --",
Expand Down
36 changes: 35 additions & 1 deletion src/api/docker-compose.yml
Expand Up @@ -32,7 +32,7 @@ services:
environment:
- IMAGE_PORT
ports:
- '${IMAGE_PORT}'
- ${IMAGE_PORT}
depends_on:
- traefik
labels:
Expand Down Expand Up @@ -82,3 +82,37 @@ services:
- 'traefik.http.middlewares.strip_auth_prefix.stripprefix.prefixes=/${API_VERSION}/auth'
- 'traefik.http.middlewares.strip_auth_prefix.stripprefix.forceSlash=true'
- 'traefik.http.routers.auth.middlewares=strip_auth_prefix'

redis:
image: redis:latest
container_name: api_redis
ports:
- '6379'
command: ['redis-server', '--appendonly', 'yes']
volumes:
- ./redis-data:/data

# posts service
posts:
container_name: 'posts'
build:
context: ./posts
dockerfile: Dockerfile
environment:
- POSTS_PORT
ports:
- ${POSTS_PORT}
depends_on:
- traefik
- redis
labels:
# Enable Traefik
- 'traefik.enable=true'
# Traefik routing for the image service at /v1/image
- 'traefik.http.routers.posts.rule=Host(`${API_HOST}`) && PathPrefix(`/${API_VERSION}/posts`)'
# Specify the posts service port
- 'traefik.http.services.posts.loadbalancer.server.port=${IMAGE_PORT}'
# Add middleware to this route to strip the /v1/posts prefix
- 'traefik.http.middlewares.strip_posts_prefix.stripprefix.prefixes=/${API_VERSION}/posts'
- 'traefik.http.middlewares.strip_posts_prefix.stripprefix.forceSlash=true'
- 'traefik.http.routers.posts.middlewares=strip_posts_prefix'
13 changes: 13 additions & 0 deletions src/api/env.development
Expand Up @@ -77,3 +77,16 @@ IMAGE_PORT=4444

# Image Service URL
IMAGE_URL=http://api.telescope.localhost/v1/image

################################################################################
# Posts Service
################################################################################

# Posts Service Port (default is 5555)
POSTS_PORT=5555

# Posts Service URL
POSTS_URL=http://api.telescope.localhost/v1/posts

# Redis Mock info
MOCK_REDIS=
13 changes: 13 additions & 0 deletions src/api/env.production
Expand Up @@ -83,3 +83,16 @@ IMAGE_PORT=4444

# Image Service URL
IMAGE_URL=https://api.telescope.cdot.systems/v1/image

################################################################################
# Posts Service
################################################################################

# Posts Service Port (default is 5555)
POSTS_PORT=5555

# Posts Service URL
POSTS_URL=http://api.telescope.localhost/v1/posts

# Redis Mock info
MOCK_REDIS=
13 changes: 13 additions & 0 deletions src/api/env.staging
Expand Up @@ -83,3 +83,16 @@ IMAGE_PORT=4444

# Image Service URL
IMAGE_URL=https://dev.api.telescope.cdot.systems/v1/image

################################################################################
# Posts Service
################################################################################

# Posts Service Port (default is 5555)
POSTS_PORT=5555

# Posts Service URL
POSTS_URL=http://api.telescope.localhost/v1/posts

# Redis Mock info
MOCK_REDIS=
6 changes: 6 additions & 0 deletions src/api/posts/.dockerignore
@@ -0,0 +1,6 @@
.dockerignore
node_modules
npm-debug.log
Dockerfile
.git
.gitignore
13 changes: 13 additions & 0 deletions src/api/posts/Dockerfile
@@ -0,0 +1,13 @@
FROM node:lts-alpine as base

RUN apk add dumb-init

WORKDIR /app

COPY --chown=node:node . .

RUN npm install --only=production --no-package-lock

USER node

CMD ["dumb-init", "node", "src/server.js"]
34 changes: 34 additions & 0 deletions src/api/posts/README.md
@@ -0,0 +1,34 @@
# Image Service

The Post Service parses posts from user's feeds to display them on Telescope.

## Install

```
npm install
```

## Usage

```
# normal mode
npm start
# dev mode with automatic restarts
npm run dev
```

By default the server is running on <http://localhost:5555/>.

### Examples

- `GET /posts` - Returns the 10 latests posts parsed

- `GET /posts/:id` - Returns information about a specific post

- `GET /healthcheck` - returns `{ "status": "ok" }` if everything is running properly

## Docker

- To build and tag: `docker build . -t telescope_posts_svc:latest`
- To run locally: `docker run -p 5555:5555 telescope_posts_svc:latest`
8 changes: 8 additions & 0 deletions src/api/posts/jest.config.js
@@ -0,0 +1,8 @@
const baseConfig = require('../../../jest.config.base');

module.exports = {
...baseConfig,
rootDir: '../../..',
testMatch: ['<rootDir>/src/api/posts/test/*.test.js'],
collectCoverageFrom: ['<rootDir>/src.api/posts/src/**/*.js'],
};
10 changes: 10 additions & 0 deletions src/api/posts/jest.setup.js
@@ -0,0 +1,10 @@
const path = require('path');
const result = require('dotenv').config({
path: path.join(__dirname, '../env.development'),
});

process.env = { ...process.env, MOCK_REDIS: '1' };

if (result.error) {
throw result.error;
}
28 changes: 28 additions & 0 deletions src/api/posts/package.json
@@ -0,0 +1,28 @@
{
"name": "@senecacdot/posts-service",
"version": "1.0.0",
"private": true,
"description": "A service for retrieving posts",
"scripts": {
"start": "node src/server.js"
},
"repository": "Seneca-CDOT/telescope",
"license": "BSD-2-Clause",
"bugs": {
"url": "https://github.com/Seneca-CDOT/telescope/issues"
},
"homepage": "https://github.com/Seneca-CDOT/telescope#readme",
"engines": {
"node": ">=12.0.0"
},
"dependencies": {
"@senecacdot/satellite": "^1.x",
"express-validator": "^6.10.0",
"ioredis": "^4.22.0",
"ioredis-mock": "^5.2.4",
"jsdom": "^16.4.0"
},
"devDependencies": {
"supertest": "^6.1.3"
}
}
99 changes: 99 additions & 0 deletions src/api/posts/src/data/post.js
@@ -0,0 +1,99 @@
const { hash } = require('@senecacdot/satellite');
const { getPost, addPost } = require('../storage');
const textParser = require('../text-parser');

/**
* Makes sure that a given date can be constructed as a Date object
* Returns a constructed Date object, if possible
* Otherwise throws an Error
* @param {Object} date an Object to construct as a Date object
* @param {Date} [fallbackDate] an optional second Date to construct in case the first fails to do so
*/
function ensureDate(date, fallbackDate) {
if (
date &&
(Object.prototype.toString.call(date) === '[object String]' ||
(Object.prototype.toString.call(date) === '[object Date]' && !Number.isNaN(date)))
) {
return new Date(date);
}
if (Object.prototype.toString.call(fallbackDate) === '[object Date]') {
return new Date(fallbackDate);
}

throw new Error(`post has an invalid date: ${date}'`);
}

class Post {
constructor(title, html, datePublished, dateUpdated, postUrl, guid, feed) {
// Use the post's guid as our unique identifier
this.id = hash(guid);
this.title = title;
this.html = html;
this.published = ensureDate(datePublished);
this.updated = ensureDate(dateUpdated, datePublished);
this.url = postUrl;
this.guid = guid;
this.feed = feed;
}

/**
* Save the current Post to the database, swapping the feed's id
* for the entire Feed object.
* Returns a Promise.
*/
save() {
return addPost({
...this,
feed: this.feed.id,
});
}

/**
* Generate the plain text version of this post on demand vs. storing
*/
get text() {
return textParser(this.html);
}

get author() {
return this.feed.author;
}

/**
* Creates a new Post object by extracting data from the given post-like object.
* @param {Object} postData - an Object containing the necessary fields. The
* feed property can be an id or a full Feed Object.
* Returns the newly created Post's id.
*/
static async create(postData) {
// If we only have a feed id, get the full Feed Object instead.
const post = new Post(
postData.title,
postData.html,
postData.published,
postData.updated,
postData.url,
postData.guid
);
await post.save();
return post.id;
}

/**
* Returns a Post from the database using the given id
* @param {String} id - the id of a post (hashed guid) to get from Redis.
*/
static async byId(id) {
const data = await getPost(id);
// No post found using this id
if (!(data && data.id)) {
return null;
}

const post = new Post(data.title, data.html, data.published, data.updated, data.url, data.guid);
return post;
}
}

module.exports = Post;
9 changes: 9 additions & 0 deletions src/api/posts/src/index.js
@@ -0,0 +1,9 @@
const { Satellite } = require('@senecacdot/satellite');

const posts = require('./routes/posts');

const service = new Satellite();

service.router.use('/', posts);

module.exports = service;
36 changes: 36 additions & 0 deletions src/api/posts/src/libs/redis.js
@@ -0,0 +1,36 @@
const Redis = require('ioredis');
const MockRedis = require('ioredis-mock');
const { logger } = require('@senecacdot/satellite');

// If you need to set the Redis URL, do it in REDIS_URL
const redisUrl = 'http://redis:6379';

// Set MOCK_REDIS=1 to mock, MOCK_REDIS= to use real redis
const useMockRedis = process.env.MOCK_REDIS;

// RedisConstructor is one of Redis or MockRedis
const RedisConstructor = useMockRedis ? MockRedis : Redis;

function createRedisClient() {
try {
const { port, host } = new URL(redisUrl);
return new RedisConstructor(port, host, { password: process.env.REDIS_PASSWORD });
} catch (error) {
const message = `Unable to parse port and host from "${redisUrl}"`;
logger.error({ error }, message);
throw new Error(message);
}
}

// If using MockRedis, shim info() until https://github.com/stipsan/ioredis-mock/issues/841 ships
if (useMockRedis && typeof MockRedis.prototype.info !== 'function') {
logger.debug('Shimming MockRedis info() method');
MockRedis.prototype.info = () => Promise.resolve('redis_version:999.999.999');
}

module.exports = {
// If callers need to create a new redis instance, they'll use the ctor
createRedisClient,
// Otherwise they can use this shared instance (most should use this)
redis: createRedisClient(),
};

0 comments on commit e4904a5

Please sign in to comment.