diff --git a/README.md b/README.md index 0e94dff..e363b4b 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,148 @@ -# News API SDK for Node (javascript) -Coming soon... this is where our officially supported SDK for Node JS is going to live. +# newsapi -*** +A node interface for NewsAPI. -News API is a simple HTTP REST API for searching and retrieving live articles from all over the web. It can help you answer questions like: +[![npm](https://img.shields.io/npm/v/newsapi.svg)](https://www.npmjs.com/package/newsapi) +[![npm](https://img.shields.io/npm/dt/newsapi.svg)](https://www.npmjs.com/package/newsapi) +[![Build Status](https://travis-ci.org/bzarras/newsapi.svg?branch=master)](https://travis-ci.org/bzarras/newsapi) -- What top stories is the NY Times running right now? -- What new articles were published about the next iPhone today? -- Has my company or product been mentioned or reviewed by any blogs recently? -- How many social shares has an article received? (Coming soon!) +Up-to-date news headlines and metadata in JSON from 70+ popular news sites. Powered by NewsAPI.org. -You can search for articles with any combination of the following criteria: +You will need an API key from [https://newsapi.org](https://newsapi.org). -- Keyword or phrase. Eg: find all articles containing the word 'Microsoft'. -- Date published. Eg: find all articles published yesterday. -- Source name. Eg: find all articles by 'TechCrunch'. -- Source domain name. Eg: find all articles published on nytimes.com. -- Language. Eg: find all articles written in English. +Please look at their [documentation](https://newsapi.org/docs) to see how to use the API. The convenience functions provided by this module +simply pass their options along as querystring parameters to the REST API, so the [documentation](https://newsapi.org/docs) +is totally valid. There are some usage examples below to see how these options should be passed in. -You can sort the results in the following orders: +If you use this in a project, add a 'powered by' attribution link back to NewsAPI.org -- Date published -- Relevancy to search keyword -- Popularity of source -- Social shares (Coming soon!) +## Add to your project +```shell +$ npm install newsapi --save +``` -You need an API key to use the API - this is a unique key that identifies your requests. They're free for development, open-source, and non-commercial use. You can get one here: [https://newsapi.org](https://newsapi.org). +## Test +```shell +$ API_KEY= npm test +``` + +## Example usage of v2 API +All methods support promises and node-style callbacks. +```js +const NewsAPI = require('newsapi'); +const newsapi = new NewsAPI('YOUR_API_KEY'); + +// To query top headlines +// All options passed to topHeadlines are optional, but you need to include at least one of them +newsapi.v2.topHeadlines({ + sources: 'bbc-news,the-verge', + q: 'trump', + category: 'politics', + language: 'en', + country: 'us' +}).then(response => { + console.log(response); + /* + { + status: "ok", + articles: [...] + } + */ +}); + +// To query everything +// You must include at least one q, source, or domain +newsapi.v2.everything({ + q: 'trump', + sources: 'bbc-news,the-verge', + domains: 'bbc.co.uk, techcrunch.com', + from: '2017-12-01', + to: '2017-12-12', + language: 'en', + sortBy: 'relevancy', + page: 2 +}).then(response => { + console.log(response); + /* + { + status: "ok", + articles: [...] + } + */ +}); + +// To query sources +// All options are optional +newsapi.v2.sources({ + category: 'technology', + language: 'en', + country: 'us' +}).then(response => { + console.log(response); + /* + { + status: "ok", + sources: [...] + } + */ +}); +``` + +## Example usage of v1 legacy API +```js +const NewsAPI = require('newsapi'); +const newsapi = new NewsAPI('YOUR_API_KEY'); + +// To query articles: +newsapi.articles({ + source: 'associated-press', // required + sortBy: 'top' // optional +}).then(articlesResponse => { + console.log(articlesResponse); + /* + { + status: "ok", + source: "associated-press", + sortBy: "top", + articles: [...] + } + */ +}); + +// To query sources: +newsapi.sources({ + category: 'technology', // optional + language: 'en', // optional + country: 'us' // optional +}).then(sourcesResponse => { + console.log(sourcesResponse); + /* + { + status: "ok", + sources: [...] + } + */ +}); + +// For both methods you can also use traditional Node callback style: +newsapi.articles({ + source: 'associated-press', + sortBy: 'top' +}, (err, articlesResponse) => { + if (err) console.error(err); + else console.log(articlesResponse); +}); +``` + +## Caching +[NewsAPI's caching behavior](https://newsapi.org/docs/caching). +You can disable caching on a request level by adding the `noCache: true` option to your queries. +```js +newsapi.v2.everything({ + sources: 'bbc-news' +}, { + noCache: true +}).then(response => { + ... +}); +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..7726cfd --- /dev/null +++ b/index.js @@ -0,0 +1,147 @@ +'use strict'; +/** + * This module provides access to the News API + * https://newsapi.org/ + * + * The API provides access to recent news headlines + * from many popular news sources. + * + * The author of this code has no formal relationship with NewsAPI.org and does not + * claim to have created any of the facilities provided by NewsAPI.org. + */ + +const Promise = require('bluebird'), + request = require('request'), + qs = require('querystring'), + host = 'https://newsapi.org'; + +let API_KEY; // To be set by clients + +class NewsAPI { + constructor (apiKey) { + if (!apiKey) throw new Error('No API key specified'); + API_KEY = apiKey; + this.v2 = { + topHeadlines (...args) { + const { params = { language: 'en' }, options, cb } = splitArgsIntoOptionsAndCallback(args); + const url = createUrlFromEndpointAndOptions('/v2/top-headlines', params); + return getDataFromWeb(url, options, API_KEY, cb); + }, + + everything (...args) { + const { params, options, cb } = splitArgsIntoOptionsAndCallback(args); + const url = createUrlFromEndpointAndOptions('/v2/everything', params); + return getDataFromWeb(url, options, API_KEY, cb); + }, + + sources (...args) { + const { params, options, cb } = splitArgsIntoOptionsAndCallback(args); + const url = createUrlFromEndpointAndOptions('/v2/sources', params); + return getDataFromWeb(url, options, API_KEY, cb); + } + } + } + + sources (...args) { + const { params, options, cb } = splitArgsIntoOptionsAndCallback(args); + const url = createUrlFromEndpointAndOptions('/v1/sources', params); + return getDataFromWeb(url, options, null, cb); + } + + articles (...args) { + const { params, options, cb } = splitArgsIntoOptionsAndCallback(args); + const url = createUrlFromEndpointAndOptions('/v1/articles', params); + return getDataFromWeb(url, options, API_KEY, cb); + } +} + +class NewsAPIError extends Error { + constructor(err) { + super(); + this.name = `NewsAPIError: ${err.code}`; + this.message = err.message; + } +} + +/** + * Takes a variable-length array that represents arguments to a function and attempts to split it into + * an 'options' object and a 'cb' callback function. + * @param {Array} args The arguments to the function + * @return {Object} + */ +function splitArgsIntoOptionsAndCallback (args) { + let params; + let options; + let cb; + if (args.length > 1) { + const possibleCb = args[args.length - 1]; + if ('function' === typeof possibleCb) { + cb = possibleCb; + options = args.length === 3 ? args[1] : undefined; + } else { + options = args[1]; + } + params = args[0]; + } else if ('object' === typeof args[0]) { + params = args[0]; + } else if ('function' === typeof args[0]) { + cb = args[0]; + } + return { params, options, cb }; +} + +/** + * Creates a url string from an endpoint and an options object by appending the endpoint + * to the global "host" const and appending the options as querystring parameters. + * @param {String} endpoint + * @param {Object} [options] + * @return {String} + */ +function createUrlFromEndpointAndOptions (endpoint, options) { + const query = qs.stringify(options); + const baseURL = `${host}${endpoint}`; + return query ? `${baseURL}?${query}` : baseURL; +} + +/** + * Takes a URL string and returns a Promise containing + * a buffer with the data from the web. + * @param {String} url A URL String + * @param {String} apiKey (Optional) A key to be used for authentication + * @return {Promise} A Promise containing a Buffer + */ +function getDataFromWeb(url, options, apiKey, cb) { + let useCallback = 'function' === typeof cb; + return new Promise((resolve, reject) => { + const req = { url, headers: {} }; + if (apiKey) { + req.headers['X-Api-Key'] = apiKey; + } + if (options && options.noCache === true) { + req.headers['X-No-Cache'] = 'true'; + } + request.get(req, (err, res, body) => { + if (err) { + if (useCallback) return cb(err); + return reject(err); + } + try { + const data = JSON.parse(body); + if (data.status === 'error') throw new NewsAPIError(data); + // 'showHeaders' option can be used for clients to debug response headers + // response will be in form of { headers, body } + if (options && options.showHeaders) { + if (useCallback) return cb(null, { headers: res.headers, body: data }); + return resolve({ headers: res.headers, body: data }); + } + if (useCallback) return cb(null, data); + return resolve(data); + } catch (e) { + if (useCallback) return cb(e); + return reject(e); + } + }); + }); +} + +module.exports = NewsAPI; diff --git a/package.json b/package.json new file mode 100644 index 0000000..6bfa51b --- /dev/null +++ b/package.json @@ -0,0 +1,83 @@ +{ + "_from": "newsapi", + "_id": "newsapi@2.2.0", + "_inBundle": false, + "_integrity": "sha512-WKqItQ3E4hyzRaPMaI1iyxzRzWNsiY7ttrR1FVsJm2y4wMlrYcXLYixPxNKT74pNtIYpBbOTjz4hiBYfcT81Ng==", + "_location": "/newsapi", + "_phantomChildren": { + "ajv": "5.5.2", + "asynckit": "0.4.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "delayed-stream": "1.0.0", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "jsprim": "1.4.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "safe-buffer": "5.1.1", + "sshpk": "1.13.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + }, + "_requested": { + "type": "tag", + "registry": true, + "raw": "newsapi", + "name": "newsapi", + "escapedName": "newsapi", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/newsapi/-/newsapi-2.2.0.tgz", + "_shasum": "660ccfd5f2719f471e6a8af0783d223d99633249", + "_spec": "newsapi", + "_where": "/var/www/html/universal-starter", + "author": { + "name": "Ben Zarras" + }, + "bugs": { + "url": "https://github.com/bzarras/newsapi/issues" + }, + "bundleDependencies": false, + "dependencies": { + "bluebird": "^3.4.6", + "request": "^2.83.0" + }, + "deprecated": false, + "description": "A node interface for the awesome News API from newsapi.org", + "devDependencies": { + "dotenv": "^5.0.0", + "mocha": "^3.1.2", + "should": "^11.1.1" + }, + "homepage": "https://github.com/bzarras/newsapi#readme", + "keywords": [ + "news", + "api", + "newsapi", + "news api" + ], + "license": "MIT", + "main": "index.js", + "name": "newsapi", + "repository": { + "type": "git", + "url": "git+https://github.com/bzarras/newsapi.git" + }, + "scripts": { + "test": "mocha" + }, + "version": "2.2.0" +} diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..534af22 --- /dev/null +++ b/test/test.js @@ -0,0 +1,174 @@ +'use strict'; + +require('dotenv').config(); + +let should = require('should'), + NewsAPI = require('../index'); + +if (!process.env.API_KEY) throw new Error('No API Key specified. Please create an environment variable named API_KEY'); +let newsapi = new NewsAPI(process.env.API_KEY); + +describe('NewsAPI', function () { + describe('V1', function () { + describe('Sources', function () { + it('should return "ok" and a list of sources', function (done) { + newsapi.sources().then(res => { + res.status.should.equal('ok'); + should.exist(res.sources); + done(); + }).catch(done); + }); + + it('should return "ok" and a list of sources using a callback', function (done) { + newsapi.sources((err, res) => { + if (err) { + return done(err); + } + res.status.should.equal('ok'); + should.exist(res.sources); + done(); + }); + }); + + it('should return "ok" and a list of sources using a callback and empty params object', function (done) { + newsapi.sources({}, (err, res) => { + if (err) { + return done(err); + } + res.status.should.equal('ok'); + should.exist(res.sources); + done(); + }); + }); + }); + + describe('Articles', function () { + it('should return "ok" and a list of articles for a valid source', function (done) { + const sourceId = 'buzzfeed'; + newsapi.articles({ + source: sourceId + }).then(articlesRes => { + articlesRes.status.should.equal('ok'); + should.exist(articlesRes.articles); + done(); + }).catch(done); + }); + + it('should return "ok" and a list of articles for a valid source using a callback', function (done) { + const sourceId = 'buzzfeed'; + newsapi.articles({ + source: sourceId + }, (err, articlesRes) => { + if (err) { + return done(err); + } + articlesRes.status.should.equal('ok'); + should.exist(articlesRes.articles); + done(); + }); + }); + + it('Should throw an error if no source is provided', function (done) { + newsapi.articles().then(res => { + done(new Error('Should have thrown an error')); + }).catch(err => { + console.log(err); + done(); + }); + }); + }); + }); + + describe('V2', function () { + describe('sources', function () { + it('Should return "ok" and a list of sources', function (done) { + newsapi.v2.sources().then(res => { + res.status.should.equal('ok'); + should.exist(res.sources); + done(); + }).catch(done); + }); + }); + + describe('top-headlines', function () { + it('Should return "ok" and a list of top headlines', function (done) { + newsapi.v2.topHeadlines({ + language: 'en' + }).then(res => { + res.status.should.equal('ok'); + should.exist(res.articles); + done(); + }).catch(done); + }); + + it('Should return "ok" and a list of top headlines using a callback', function (done) { + newsapi.v2.topHeadlines({ + language: 'en' + }, (err, res) => { + if (err) { + return done(err); + } + res.status.should.equal('ok'); + should.exist(res.articles); + done(); + }); + }); + + it('Should default to english language if no options are provided and return a list of top headlines', function (done) { + newsapi.v2.topHeadlines().then(res => { + res.status.should.equal('ok'); + should.exist(res.articles); + done(); + }).catch(done); + }); + + it('Should throw an error if all required params are missing', function (done) { + newsapi.v2.topHeadlines({}) + .then(res => { + done(new Error('This should have thrown an error')); + }) + .catch((err) => { + console.log(err); + done() + }); + }); + }); + + describe('everything', function () { + it('Should return "ok" and a list of articles', function (done) { + newsapi.v2.everything({ + sources: 'bbc-news' + }).then(res => { + res.status.should.equal('ok'); + should.exist(res.articles); + done(); + }).catch(done); + }); + + it('Should not cache results if noCache is on', function (done) { + newsapi.v2.everything({ + sources: 'bbc-news' + }, { + noCache: true, + showHeaders: true + }).then(res => { + res.headers['x-cached-result'].should.equal('false'); + res.body.status.should.equal('ok'); + should.exist(res.body.articles); + done(); + }).catch(done); + }); + + it('Should throw an error if all required params are missing', function (done) { + newsapi.v2.everything({}) + .then(res => { + done(new Error('This should have thrown an error')); + }) + .catch((err) => { + console.log(err); + done() + }); + }); + }); + }); +});