Skip to content

Commit

Permalink
Fixes for API
Browse files Browse the repository at this point in the history
  • Loading branch information
juliantoledo committed Mar 19, 2018
1 parent 460f08f commit 16ed641
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 91 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.json
Expand Up @@ -31,5 +31,8 @@
// http://eslint.org/docs/user-guide/configuring#specifying-environments
"env": {
"node": true
},
"parserOptions": {
"ecmaVersion": 2017
}
}
154 changes: 100 additions & 54 deletions controllers/api/pwa.js
Expand Up @@ -18,28 +18,11 @@
const express = require('express');
require('express-csv');
const pwaLib = require('../../lib/pwa');
const libMetadata = require('../../lib/metadata');
const router = express.Router(); // eslint-disable-line new-cap
const CACHE_CONTROL_EXPIRES = 60 * 60 * 1; // 1 hour
const RSS = require('rss');

const config = require('../../config/config');
const apiKeyArray = config.get('API_TOKENS');

/**
* Checks for the presence of an API key from API_TOKENS in config.json
*
* Skip API key check of RSS feed
*/
function checkApiKey(req, res, next) {
if (req.query.key &&
(apiKeyArray === req.query.key ||
apiKeyArray.indexOf(req.query.key) !== -1) ||
req.query.format === 'rss') {
return next();
}
return res.sendStatus(403);
}

function getDate(date) {
return new Date(date).toISOString().split('T')[0];
}
Expand Down Expand Up @@ -88,34 +71,83 @@ class JsonWriter {
}
}

function render(res, view, options) {
return new Promise((resolve, reject) => {
res.render(view, options, (err, html) => {
if (err) {
console.log(err);
reject(err);
}
resolve(html);
});
});
}

function renderOnePwaRss(pwa, req, res) {
const url = req.originalUrl;
const contentOnly = false || req.query.contentOnly;
let arg = Object.assign(libMetadata.fromRequest(req, url), {
pwa: pwa,
title: 'PWA Directory: ' + pwa.name,
description: 'PWA Directory: ' + pwa.name + ' - ' + pwa.description,
backlink: true,
contentOnly: contentOnly
});
return render(res, 'pwas/view-rss.hbs', arg);
}

async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}

class RssWriter {
write(result, pwas) {
write(req, res, pwas) {
const feed = new RSS({
/* eslint-disable camelcase */
title: 'PWA Directory',
description: 'A Directory of Progressive Web Apps',
feed_url: 'https://pwa-directory.appspot.com/api/pwa?format=rss',
feed_url: 'https://pwa-directory.appspot.com/api/pwa/?format=rss',
site_url: 'https://pwa-directory.appspot.com/',
image_url: 'https://pwa-directory.appspot.com/favicons/android-chrome-144x144.png',
pubDate: new Date(),
custom_namespaces: {
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
l: 'http://purl.org/rss/1.0/modules/link/',
media: 'http://search.yahoo.com/mrss/',
content: 'http://purl.org/rss/1.0/modules/content/'
}
});

pwas.forEach(pwa => {
feed.item({
title: pwa.displayName,
description: pwa.description,
url: 'https://pwa-directory.appspot.com/pwas/' + pwa.id,
guid: pwa.id,
date: pwa.created,
custom_elements: [{'content:encoded': JSON.stringify(pwa)}]
const start = async _ => {
await asyncForEach(pwas, async pwa => {
let html = await renderOnePwaRss(pwa, req, res);

const customElements = [];
customElements.push({'content:encoded': html});
customElements.push({'l:link': {_attr: {'l:rel': 'http://purl.org/rss/1.0/modules/link/#alternate',
'l:type': 'application/json',
'rdf:resource': 'https://pwa-directory.appspot.com/api/pwa/' + pwa.id}}});
if (pwa.iconUrl128) {
customElements.push({'media:thumbnail': {_attr: {url: pwa.iconUrl128,
height: '128', width: '128'}}});
}

feed.item({
title: pwa.displayName,
url: 'https://pwa-directory.appspot.com/pwas/' + pwa.id,
description: html,
guid: pwa.id,
date: pwa.created,
custom_elements: customElements
});
});
});
res.setHeader('Content-Type', 'application/rss+xml');
res.status(200).send(feed.xml());
};
start();
/* eslint-enable camelcase */
result.setHeader('Content-Type', 'application/rss+xml');
result.status(200).send(feed.xml());
}
}

Expand All @@ -128,34 +160,48 @@ const rssWriter = new RssWriter();
*
* Returns all PWAs as JSON or ?format=csv for CSV.
*/
router.get('/', checkApiKey, (req, res) => {
router.get('/:id*?', (req, res) => {
let format = req.query.format || 'json';
let sort = req.query.sort || 'newest';
let skip = parseInt(req.query.skip, 10);
let limit = parseInt(req.query.limit, 10);

let limit = parseInt(req.query.limit, 10) || 100;
res.setHeader('Cache-Control', 'public, max-age=' + CACHE_CONTROL_EXPIRES);
pwaLib.list(skip, limit, sort)
.then(result => {
switch (format) {
case 'csv': {
csvWriter.write(res, result.pwas);
break;
}
case 'rss': {
rssWriter.write(res, result.pwas);
break;
}
default: {
jsonWriter.write(res, result.pwas);
}

return new Promise((resolve, reject) => {
if (req.params.id) { // Single PWA
pwaLib.find(req.params.id)
.then(onePwa => {
resolve({pwas: [onePwa]});
})
.catch(err => {
console.log(err);
res.status(404);
res.json(err);
});
} else {
resolve(pwaLib.list(skip, limit, sort));
}
})
.then(result => {
switch (format) {
case 'csv': {
csvWriter.write(res, result.pwas);
break;
}
})
.catch(err => {
console.log(err);
res.status(500);
res.json(err);
});
case 'rss': {
rssWriter.write(req, res, result.pwas);
break;
}
default: {
jsonWriter.write(res, result.pwas);
}
}
})
.catch(err => {
console.log(err);
res.status(500);
res.json(err);
});
});

module.exports = router;
37 changes: 0 additions & 37 deletions test/app/controllers/api/pwa.js
Expand Up @@ -56,30 +56,6 @@ describe('controllers.api.pwa', () => {
// simpleMock.restore();
});

it('respond with 400 without API key', done => {
simpleMock.mock(libPwa, 'list');
// /api/ is part of the router, we need to start from /pwa/
request(app)
.get('/pwa/')
.expect(400)
.expect('Content-Type', 'text/plain; charset=utf-8').should.be.rejected.then(_ => {
assert.equal(libPwa.list.callCount, 0);
done();
});
});

it('respond with 400 with wrong API key', done => {
simpleMock.mock(libPwa, 'list');
// /api/ is part of the router, we need to start from /pwa/
request(app)
.get('/pwa?key=xxxxxxx')
.expect(400)
.expect('Content-Type', 'text/plain; charset=utf-8').should.be.rejected.then(_ => {
assert.equal(libPwa.list.callCount, 0);
done();
});
});

it('respond with 200 and json', done => {
simpleMock.mock(libPwa, 'list').resolveWith(Promise.resolve(result));
// /api/ is part of the router, we need to start from /pwa/
Expand All @@ -103,18 +79,5 @@ describe('controllers.api.pwa', () => {
done();
});
});

it('respond with 200 and rss, without key', done => {
simpleMock.mock(libPwa, 'list').resolveWith(Promise.resolve(result));
// /api/ is part of the router, we need to start from /pwa/
request(app)
.get('/pwa?format=rss')
.expect(200)
.expect('Content-Type', 'application/rss+xml; charset=utf-8')
.should.be.fulfilled.then(_ => {
assert.equal(libPwa.list.callCount, 1);
done();
});
});
});
});
14 changes: 14 additions & 0 deletions views/pwas/view-rss.hbs
@@ -0,0 +1,14 @@
<div style="text-align: center; vertical-align: middle; background-color:{{pwa.backgroundColor}}; color:{{contrastColor pwa.backgroundColor}}; padding: 16px;">
<a href="{{pwa.absoluteStartUrl}}" target="_blank" rel="noopener">
{{#if pwa.iconUrl128}}
<img src="{{pwa.iconUrl128}}" width="128" height="128"/>
{{/if}}
</a>
<a href="{{pwa.absoluteStartUrl}}" target="_blank" rel="noopener" style="color:{{contrastColor pwa.backgroundColor}}; text-decoration: none;"><h2>{{pwa.displayName}}</h2></a>
<div>{{pwa.description}}</div>
</div>

<a href="https://pwa-directory.appspot.com/pwas/{{pwa.id}}" target="_blank" rel="noopener">
<img style="vertical-align:middle" src="https://pwa-directory.appspot.com/favicons/android-chrome-36x36.png">
<span style="">Open in PWA Directory</span>
</a>

0 comments on commit 16ed641

Please sign in to comment.