-
Notifications
You must be signed in to change notification settings - Fork 188
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
612 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.dockerignore | ||
node_modules | ||
npm-debug.log | ||
Dockerfile | ||
.git | ||
.gitignore |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
FROM node:lts-alpine as base | ||
|
||
# https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/ | ||
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Videos Service | ||
|
||
The Video Service returns YouTube links to videos uploaded from user's registered channels. | ||
Its main purpose is to embed a YouTube video player on Telescope. | ||
This service is really similar to the Post service. | ||
|
||
## Install | ||
|
||
``` | ||
npm install | ||
``` | ||
|
||
## Usage | ||
|
||
``` | ||
# normal mode | ||
npm start | ||
# dev mode with automatic restarts | ||
npm run dev | ||
``` | ||
|
||
By default, the server will run on <http://loclahost:11234/>. | ||
|
||
### Available routes with examples | ||
|
||
- `GET /videos` - Returns the 10 latests videos tracked. Query parameters: | ||
- `page` - a number. It is a number from 1 or greater. It specifies the page you want to see | ||
from the results. | ||
- `per_page` - a number. It is a number from 1 or greater. It specifies how many videos should be returned by page. | ||
- `GET /videos/:id` - Returns the information associated to the video ID. | ||
|
||
## Docker | ||
|
||
- To build and tag: `docker build . -t telescope_videos_svc:latest` | ||
- To run locally: `docker run -p 11234:11234 telescope_videos_svc:latest` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
const baseConfig = require('../../../jest.config.base'); | ||
|
||
module.exports = { | ||
...baseConfig, | ||
rootDir: '../../..', | ||
testMatch: ['<rootDir>/src/api/videos/test/e2e/*.test.js'], | ||
collectCoverageFrom: ['<rootDir>/src/api/videos/src/**/*.js'], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
const baseConfig = require('../../../jest.config.base'); | ||
|
||
module.exports = { | ||
...baseConfig, | ||
rootDir: '../../..', | ||
testMatch: ['<rootDir>/src/api/videos/test/*.test.js'], | ||
collectCoverageFrom: ['<rootDir>/src/api/videos/src/**/*.js'], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"name": "@senecacdot/videos-service", | ||
"version": "1.0.0", | ||
"private": true, | ||
"description": "A service for retrieving videos from YouTube", | ||
"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.25.0", | ||
"ioredis-mock": "^5.4.1", | ||
"jsdom": "^18.0.0", | ||
"normalize-url": "^6.0.1" | ||
}, | ||
"devDependencies": { | ||
"supertest": "^6.1.3" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
const { Satellite } = require('@senecacdot/satellite'); | ||
|
||
const service = new Satellite(); | ||
const videos = require('./routes/videos'); | ||
|
||
service.router.use('/', videos); | ||
|
||
module.exports = service; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
const normalizeUrl = require('normalize-url'); | ||
const { hash } = require('@senecacdot/satellite'); | ||
|
||
const { getFeed } = require('../storage'); | ||
|
||
const urlToId = (url) => hash(normalizeUrl(url)); | ||
|
||
class Feed { | ||
constructor(author, url, user, link, etag, lastModified) { | ||
if (!url) { | ||
throw new Error('missing url for feed'); | ||
} | ||
if (!author) { | ||
throw new Error('missing author for feed'); | ||
} | ||
// Use the feed's normalized url as our unique identifier | ||
this.id = urlToId(url); | ||
this.author = author; | ||
this.url = url; | ||
this.user = user; | ||
this.link = link; | ||
|
||
// We may or may not have these cache values when we create a feed. | ||
this.etag = etag === '' ? null : etag; | ||
this.lastModified = lastModified === '' ? null : lastModified; | ||
} | ||
|
||
/** | ||
* Returns a Feed from the database using the given id | ||
* @param {String} id - the id of a feed (hashed, normalized url) to get from Redis. | ||
* Returns a Promise<Feed> | ||
*/ | ||
static async byId(id) { | ||
const data = await getFeed(id); | ||
// No feed found using this id | ||
if (!(data && data.id)) { | ||
return null; | ||
} | ||
return new Feed(data.author, data.url, data.user, data.link, data.etag, data.lastModified); | ||
} | ||
} | ||
|
||
module.exports = Feed; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
const Feed = require('./feed'); | ||
const { hash } = require('@senecacdot/satellite'); | ||
const { getVideo } = require('../storage'); | ||
|
||
/** | ||
* 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(`video has an invalid date: ${date}'`); | ||
} | ||
|
||
/** | ||
* Makes sure that the given feed is a Feed and not just an id. If the latter | ||
* it gets the full feed. | ||
* @param {Feed|String} feed a Feed object or feed id | ||
* Returns a Promise<Feed> | ||
*/ | ||
function ensureFeed(feed) { | ||
return feed instanceof Feed ? Promise.resolve(feed) : Feed.byId(feed); | ||
} | ||
|
||
class Video { | ||
constructor(title, url, guid, datePublished, dateUpdated, feed) { | ||
this.id = hash(guid); | ||
this.title = title; | ||
this.url = url; | ||
this.guid = guid; | ||
this.published = ensureDate(datePublished); | ||
this.updated = ensureDate(dateUpdated, datePublished); | ||
|
||
if (!(feed instanceof Feed)) { | ||
throw new Error(`expected feed to be a Feed Object, got '${feed}'`); | ||
} | ||
this.feed = feed; | ||
} | ||
|
||
static async byId(id) { | ||
const data = await getVideo(id); | ||
|
||
if (!(data && data.id)) { | ||
return null; | ||
} | ||
|
||
const feed = await ensureFeed(data.feed); | ||
const video = new Video(data.title, data.url, data.guid, data.published, data.updated, feed); | ||
return video; | ||
} | ||
} | ||
|
||
module.exports = Video; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
const { Redis } = require('@senecacdot/satellite'); | ||
|
||
const redis = Redis(); | ||
|
||
module.exports = redis; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
const { Router, createError, logger } = require('@senecacdot/satellite'); | ||
const Video = require('../models/video'); | ||
const { getVideos, getVideosCount } = require('../storage'); | ||
const { validateVideosQuery, validateVideosIdParam } = require('../validation'); | ||
|
||
const videosUrl = process.env.VIDEOS_URL || '/'; | ||
|
||
const videos = Router(); | ||
|
||
videos.get('/', validateVideosQuery(), async (req, res, next) => { | ||
const defaultNumberOfVideos = process.env.MAX_VIDEOS_PER_PAGE || 30; | ||
const capNumOfVideos = 100; | ||
const page = parseInt(req.query.page || 1, 10); | ||
|
||
let ids; | ||
let perPage; | ||
let videosCount = 0; | ||
let from; | ||
let to; | ||
|
||
if (req.query.per_page) { | ||
perPage = req.query.per_page > capNumOfVideos ? capNumOfVideos : req.query.per_page; | ||
} else { | ||
perPage = defaultNumberOfVideos; | ||
} | ||
|
||
try { | ||
videosCount = await getVideosCount(); | ||
|
||
from = perPage * (page - 1); | ||
|
||
to = perPage * page > videosCount ? videosCount : perPage * page; | ||
|
||
ids = await getVideos(from, to); | ||
} catch (err) { | ||
logger.error({ err }, 'Unable to get videos from Redis'); | ||
next(createError(503, 'Unable to connect to database')); | ||
} | ||
|
||
const nextPage = to >= videosCount ? 1 : page + 1; | ||
const prevPage = from === 0 ? Math.floor(videosCount / perPage) : page - 1; | ||
|
||
res.set('X-Total-Count', videosCount); | ||
|
||
res.links({ | ||
next: `/videos?per_page=${perPage}&page=${nextPage}`, | ||
prev: `/videos?per_page=${perPage}&page=${prevPage}`, | ||
first: `/videos?per_page=${perPage}&page=${1}`, | ||
last: `/videos?per_page=${perPage}&page=${Math.floor(videosCount / perPage)}`, | ||
}); | ||
|
||
res.json(ids.map((id) => ({ id, url: `${videosUrl}/${id}` }))); | ||
}); | ||
|
||
videos.get('/:id', validateVideosIdParam(), async (req, res, next) => { | ||
const { id } = req.params; | ||
|
||
try { | ||
const video = await Video.byId(id); | ||
|
||
if (!video) { | ||
res.status(404).json({ | ||
message: `Video not found for id ${id}`, | ||
}); | ||
} else { | ||
switch (req.accepts(['json'])) { | ||
case 'json': | ||
res.append('Content-type', 'application/json').json(video); | ||
break; | ||
default: | ||
res.status(406).json({ | ||
message: 'Invalid content type', | ||
}); | ||
break; | ||
} | ||
} | ||
} catch (err) { | ||
logger.error({ err }, 'Unable to get videos from Redis'); | ||
next(createError(503, 'Unable to connect to database')); | ||
} | ||
}); | ||
|
||
module.exports = videos; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
const { logger } = require('@senecacdot/satellite'); | ||
const service = require('.'); | ||
|
||
const PORT = parseInt(process.env.VIDEOS_PORT || 11234, 10); | ||
|
||
service.start(PORT, () => { | ||
logger.debug(`Videos microservice started on port ${PORT}`); | ||
}); | ||
|
||
module.exports = service; |
Oops, something went wrong.