diff --git a/README.md b/README.md index 5edb561..b0055a2 100644 --- a/README.md +++ b/README.md @@ -11,95 +11,68 @@ Search and lookup art in various archives across Europe. * [API documentation](https://pro.europeana.eu/) -Example -------- +## Usage example ```js -const europeana = require ('europeana') ('abc123'); +const EuropeanaAPI = require ('europeana'); +const europeana = new EuropeanaAPI ({ + wskey: 'abc123', +}); + +// console.log is too limited +function out (data) { + console.dir (data, { + depth: null, + colors: true, + }); +} + // Search -const params = { +europeana.search ({ query: 'et in arcadia ego', rows: 5, -}; - -europeana ('search', params, console.log); +}) + .then (out) + .catch (console.error) +; // Record -const recordId = '/08501/03F4577D418DC84979C4E2EE36F99FECED4C7B11'; - -europeana (`record${recordId}`, console.log); +europeana.getRecord ({ + id: '08501/03F4577D418DC84979C4E2EE36F99FECED4C7B11', +}) + .then (out) + .catch (console.error) +; ``` -Installation ------------- - -`npm i europeana` - - -Configuration -------------- - -You _must_ specify an API key which you can request **[here](http://labs.europeana.eu/api/registration)** - -param | type | required | default | description -:-------|:-------|:---------|:--------|:---------------------- -apikey | string | yes | | Your API key. Do not use your private key. -timeout | number | no | 5000 | Request time out in ms - - -#### Example - -```js -const apikey = 'abc123'; -const timeout = 3000; - -var europeana = require ('europeana') (apikey, timeout); -``` +## Installation +`npm install europeana` -Callback --------- -Each method requires a callback _function_ to receive the results. +## Configuration -It receives two parameters: `err` and `data`. +You _must_ specify an API key which you can request +**[here](http://pro.europeana.eu/api/registration)** -property | type | default | description -:--------|:-------|:--------|:----------------------------- -err | Error | null | Includes `.code` and `.error` -data | Object | | Result object +param | type | default | description +:---------|:-------|:--------|:----------- +wskey | string | | API key. Do not use your private key. +[timeout] | number | `5000` | Request timeout in ms -#### Example - ```js -function myCallback (err, data) { - if (err) return console.error (err); - - console.dir (data, { - depth: null, - colors: true, - }); -} - -// Search -europeana ('search', { query: 'vincent van gogh' }, myCallback); +const EuropeanaAPI = require ('europeana'); +const europeana = new EuropeanaAPI ({ + wskey: 'abc123', + timeout: 5000, +}); ``` -#### Errors - -message | description | additional -:----------------|:-----------------------------|:----------------------- -apikey missing | You did not set your API key | -request failed | The request failed | `err.error` -invalid response | API returned invalid data | -API error | API returned an error | `err.error`, `err.code` - - Unlicense --------- diff --git a/europeana.js b/europeana.js index c7604e3..7725dca 100644 --- a/europeana.js +++ b/europeana.js @@ -6,158 +6,194 @@ Source: https://github.com/fvdm/nodejs-europeana License: Public Domain / Unlicense (see UNLICENSE file) */ -var http = require ('httpreq'); +const { doRequest } = require ('httpreq'); + +module.exports = class Europeana { + + /** + * Configuration + * + * @param {object} config + * @param {string} config.wskey API KEY + * @param {number} [config.timeout=15000] Request timeout in ms + */ + + constructor ({ + wskey, + timeout = 15000, + }) { + this._config = { + wskey, + timeout, + }; + + this._errors = { + 400: 'The request sent by the client was syntactically incorrect', + 401: 'Authentication credentials were missing or authentication failed.', + 404: 'The requested record was not found.', + 429: 'The request could be served because the application has reached its usage limit.', + 500: 'Internal Server Error. Something has gone wrong, please report to us.', + }; + } -var settings = { - apikey: null, - timeout: 5000 -}; -// errors -// http://www.europeana.eu/portal/api-working-with-api.html#Error-Codes + /** + * Search records + * + * @param {object} parameters Method parameters + * + * @return {Promise} + */ + + async search (parameters) { + return this._talk ({ + method: 'GET', + url: 'https://api.europeana.eu/record/v2/search.json', + parameters, + }); + } -var errors = { - 400: 'The request sent by the client was syntactically incorrect', - 401: 'Authentication credentials were missing or authentication failed.', - 404: 'The requested record was not found.', - 429: 'The request could be served because the application has reached its usage limit.', - 500: 'Internal Server Error. Something has gone wrong, please report to us.' -}; + /** + * Get a record + * + * @param {string} id Record ID + * + * @return {Promise} + */ -/** - * Make and call back error - * - * @callback callback - * @param message {string} - Error.message - * @param err {mixed} - Error.error - * @param res {object} - httpreq response details - * @param callback {function} - `function (err) {}` - * @returns {void} - */ - -function doError (message, err, res, callback) { - var error = new Error (message); - var code = res && res.statusCode; - var body = res && res.body; - - error.code = code; - error.error = err || errors [code] || null; - error.data = body; - callback (error); -} - - -/** - * Process response - * - * @callback callback - * @param err {Error, null} - httpreq error - * @param res {object} - httpreq response details - * @param callback {function} - `function (err, data) {}` - * @returns {void} - */ - -function httpResponse (err, res, callback) { - var data = res && res.body; - var html; - - // client failed - if (err) { - doError ('request failed', err, res, callback); - return; + async getRecord ({ id }) { + return this._talk ({ + method: 'GET', + url: `https://api.europeana.eu/record/v2/${id}.json`, + }); } - // parse response - try { - data = JSON.parse (data); - } catch (reason) { - // weird API error - if (data.match (/

HTTP Status /)) { - html = data.replace (/.*description<\/b> (.+)<\/u><\/p>.*/, '$1'); - doError ('API error', html, res, callback); - } else { - doError ('invalid response', reason, res, callback); - } - return; - } - if (data.apikey) { - delete data.apikey; - } + /** + * Generate record thumbnail URL + * + * @param {string} uri + * @param {string} type + * @param {string} size + * + * @return {Promise} + */ - // API error - if (!data.success && data.error) { - doError ('API error', data.error, res, callback); - return; - } + async getRecordThumbnailUrl ({ uri, type, size }) { + uri = encodeURIComponent (uri); - if (res.statusCode >= 300) { - doError ('API error', null, res, callback); - return; + return `https://api.europeana.eu/thumbnail/v2/url.json?uri=${uri}&type=${type}&size=${size}`; } - // all good - callback (null, data); -} - - -/** - * Communicate with API - * - * @callback callback - * @param path {string} - Method path between `/v2/` and `.json` - * @param fields {object} - Method parameters - * @param callback {function} - `function (err, data) {}` - * @returns {void} - */ - -function httpRequest (path, fields, callback) { - var options = { - method: 'GET', - url: 'https://www.europeana.eu/api/v2/' + path + '.json', - parameters: fields, - timeout: settings.timeout, - headers: { - 'User-Agent': 'europeana.js' - } - }; - if (typeof fields === 'function') { - callback = fields; - options.parameters = {}; + /** + * Get an entity + * + * @param {string} type + * @param {string} scheme + * @param {string} id + * + * @return {Promise} + */ + + async getEntity ({ type, scheme, id }) { + return this._talk ({ + method: 'GET', + url: `https://www.europeana.eu/api/entities/${type}/${scheme}/${id}.json`, + }); } - // Request - // check API key - if (!settings.apikey) { - callback (new Error ('apikey missing')); - return; + /** + * Resolve an external URI to an entity + * + * @param {string} uri + * + * @return {Promise} + */ + + async resolveEntity ({ uri }) { + uri = encodeURIComponent (uri); + + return this._talk ({ + method: 'GET', + url: 'https://www.europeana.eu/api/entities/resolve', + parameters: { + uri, + }, + }); } - options.parameters.wskey = settings.apikey; - function doResponse (err, res) { - httpResponse (err, res, callback); + /** + * Get suggestions for entities + * + * @param {object} parameters Method parameters + * + * @return {Promise} + */ + + async suggestEntities (parameters) { + return this._talk ({ + method: 'GET', + url: 'https://www.europeana.eu/api/entities/suggest', + parameters, + }); } - http.doRequest (options, doResponse); -} + /** + * Communicate with API + * + * @param {string} url Request URL + * @param {string} [method=GET] HTTP method + * @param {object} [parameters] Request parameters + * @param {number} [timeout=15000] Request timeout in ms + * + * @return {Promise} + */ + + async _talk ({ + url, + method = 'GET', + parameters = {}, + timeout = this._config.timeout, + }) { + const options = { + url, + method, + parameters, + timeout, + headers: { + 'User-Agent': 'nodejs-europeana', + }, + }; + + parameters.wskey = this._config.wskey; + + const res = await doRequest (options); + + if (res.body.match (/^ { +dotest.add ('Interface', test => { test() - .isFunction ('fail', 'exports', app) - .isFunction ('fail', 'module', europeana) - .done(); + .isClass ('fail', 'exports', pkg) + .isFunction ('fail', 'search', app && app.search) + .isFunction ('fail', 'getRecord', app && app.getRecord) + .isFunction ('fail', 'getRecordThumbnailUrl', app && app.getRecordThumbnailUrl) + .isFunction ('fail', 'getEntity', app && app.getEntity) + .isFunction ('fail', 'resolveEntity', app && app.resolveEntity) + .isFunction ('fail', 'suggestEntities', app && app.suggestEntities) + .done() + ; }); -dotest.add ('search', test => { - const props = { - query: 'who:"laurent de la hyre"' - }; +dotest.add ('search', async test => { + try { + const data = await app.search ({ + query: 'who:"laurent de la hyre"', + }); - europeana ('search', props, (err, data) => { - test (err) + test() .isObject ('fail', 'data', data) .isNotEmpty ('fail', 'data', data) .isExactly ('fail', 'data.success', data && data.success, true) .isArray ('fail', 'data.items', data && data.items) .isNotEmpty ('warn', 'data.items', data && data.items) - .done(); - }); + .done() + ; + } + + catch (err) { + test (err).done(); + } }); -dotest.add ('record', test => { - const record = '9200365/BibliographicResource_1000055039444'; - const props = { - profile: 'params' - }; +dotest.add ('getRecord', async test => { + try { + const data = await app.getRecord ({ + id: '9200365/BibliographicResource_1000055039444', + }); - europeana ('record/' + record, props, (err, data) => { - test (err) + test () .isObject ('fail', 'data', data) .isNotEmpty ('fail', 'data', data) .isExactly ('fail', 'data.success', data && data.success, true) .isObject ('fail', 'data.object', data && data.object) .isNotEmpty ('warn', 'data.object', data && data.object) - .done(); - }); -}); + .done() + ; + } - -dotest.add ('translateQuery', test => { - const props = { - languageCodes: 'nl,en,hu', - term: 'painting' - }; - - europeana ('translateQuery', props, (err, data) => { - test (err) - .isObject ('fail', 'data', data) - .isNotEmpty ('fail', 'data', data) - .isExactly ('fail', 'data.success', data && data.success, true) - .isArray ('warn', 'data.translations', data && data.translations) - .done(); - }); + catch (err) { + test (err).done(); + } }); -dotest.add ('providers normal', test => { - europeana ('providers', (err, data) => { - test (err) - .isObject ('fail', 'data', data) - .isNotEmpty ('fail', 'data', data) - .isExactly ('fail', 'data.success', data && data.success, true) - .isArray ('warn', 'data.items', data && data.items) - .done(); - }); +dotest.add ('getRecordThumbnailUrl', async test => { + try { + const data = await app.getRecordThumbnailUrl ({ + uri: 'https://www.dropbox.com/s/8gpbipwr4ipwj37/Austria_Gerstl.jpg?raw=1', + type: 'IMAGE', + size: 'w400', + }); + + test () + .isString ('fail', 'data', data) + .isExactly ('fail', 'data', data, 'https://api.europeana.eu/thumbnail/v2/url.json?uri=https%3A%2F%2Fwww.dropbox.com%2Fs%2F8gpbipwr4ipwj37%2FAustria_Gerstl.jpg%3Fraw%3D1&type=IMAGE&size=w400') + .done() + ; + } + + catch (err) { + test (err).done(); + } }); -dotest.add ('providers params', test => { - const params = { - pagesize: 3 - }; - - europeana ('providers', params, (err, data) => { - const items = data && data.items; +dotest.add ('API error - HTML', async test => { + let error; + let data; - test (err) - .isObject ('fail', 'data', data) - .isNotEmpty ('fail', 'data', data) - .isExactly ('fail', 'data.success', data && data.success, true) - .isArray ('fail', 'data.items', items) - .isExactly ('warn', 'data.items.length', items && items.length, 3) - .done(); - }); -}); + try { + data = await app.getRecord ({ + id: '-', + }); + } + catch (err) { + error = err; + } -dotest.add ('Error: API error', test => { - europeana ('record/-', (err, data) => { + finally { test() - .isError ('fail', 'err', err) - .isExactly ('fail', 'err.message', err && err.message, 'API error') - .isNumber ('fail', 'err.code', err && err.code) - .isString ('fail', 'err.error', err && err.error) + .isError ('fail', 'error', error) + .isNotEmpty ('fail', 'error.message', error && error.message) + .isExactly ('fail', 'error.code', error && error.code, 404) .isUndefined ('fail', 'data', data) - .done(); - }); + .done() + ; + } }); -dotest.add ('Error: request failed', test => { - const tmp = app (apikey, 1); +dotest.add ('API error - normal', async test => { + let error; + let data; - tmp ('providers', (err, data) => { + try { + data = await app.getRecord ({ + id: '1111111/TEST', + }); + } + + catch (err) { + error = err; + } + + finally { test() - .isError ('fail', 'err', err) - .isExactly ('fail', 'err.message', err && err.message, 'request failed') - .isError ('fail', 'err.error', err && err.error) + .isError ('fail', 'error', error) + .isNotEmpty ('fail', 'error.message', error && error.message) + .isExactly ('fail', 'error.code', error && error.code, 404) .isUndefined ('fail', 'data', data) - .done(); - }); + .done() + ; + } }); -dotest.add ('Error: apikey missing', test => { - const tmp = app(); +dotest.add ('Error: request timeout', async test => { + let error; + let data; - tmp ('providers', (err, data) => { - test() - .isError ('fail', 'err', err) - .isExactly ('fail', 'err.message', err && err.message, 'apikey missing') - .isUndefined ('fail', 'data', data) - .done(); - }); -}); + try { + const tmp = new pkg ({ + wskey, + timeout: 1, + }); + data = await tmp.getRecord ({ + id: '9200365/BibliographicResource_1000055039444', + }); + } -/* -// Suggestions is unavailable -// http://labs.europeana.eu/api/suggestions -dotest.add ('suggestions', test => { - const props = { - query: 'laurent de la hyre', - rows: 10 - }; + catch (err) { + error = err; + } - app ('suggestions', props, (err, data) => { - test (err) - .isObject ('fail', 'data', data) - .done(); - }); + finally { + test() + .isError ('fail', 'error', error) + .isExactly ('fail', 'error.code', error && error.code, 'TIMEOUT') + .isUndefined ('fail', 'data', data) + .done() + ; + } }); -*/ + // Start the tests dotest.run(500);