UNPKG

pryv

Version:

Pryv JavaScript library

434 lines (401 loc) 13.2 kB
/** * @license * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE) */ const utils = require('./utils.js'); const jsonParser = require('./lib/json-parser'); const browserGetEventStreamed = require('./lib/browser-getEventStreamed'); /** * @class Connection * A connection is an authenticated link to a Pryv.io account. * * @type {TokenAndEndpoint} * * @example * create a connection for the user 'tom' on 'pryv.me' backend with the token 'TTZycvBTiq' * const conn = new pryv.Connection('https://TTZycvBTiq@tom.pryv.me'); * * @property {string} [token] * @property {string} endpoint * @memberof pryv * * @constructor * @this {Connection} * @param {APIEndpoint} apiEndpoint * @param {pryv.Service} [service] - eventually initialize Connection with a Service */ class Connection { constructor (apiEndpoint, service) { const { token, endpoint } = utils.extractTokenAndAPIEndpoint(apiEndpoint); this.token = token; this.endpoint = endpoint; this.options = {}; this.options.chunkSize = 1000; this._deltaTime = { value: 0, weight: 0 }; if (service && !(service instanceof Service)) { throw new Error('Invalid service param'); } this._service = service; } /** * get pryv.Service object relative to this connection * @readonly * @property {pryv.Service} service */ get service () { if (this._service) return this._service; this._service = new Service(this.endpoint + 'service/info'); return this._service; } /** * get username. * It's async as in it constructed from access info * @param {*} arrayOfAPICalls * @param {*} progress */ async username () { const accessInfo = await this.accessInfo(); return accessInfo.user.username; } /** * get access info * It's async as it is constructed with get function. */ async accessInfo () { return this.get('access-info', null); } /** * Issue a Batch call https://api.pryv.com/reference/#call-batch . * arrayOfAPICalls will be splited in multiple calls if the size is > `conn.options.chunkSize` . * Default chunksize is 1000. * @param {Array.<MethodCall>} arrayOfAPICalls Array of Method Calls * @param {Function} [progress] Return percentage of progress (0 - 100); * @returns {Promise<Array>} Promise to Array of results matching each method call in order */ async api (arrayOfAPICalls, progress) { function httpHandler (batchCall) { return this.post('', batchCall); } return await this._chunkedBatchCall( arrayOfAPICalls, progress, httpHandler.bind(this) ); } /** * Make one api Api call * @param {string} method - methodId * @param {Object|Array} [params] - the params associated with this methodId * @param {string} [resultKey] - if given, returns the value or throws an error if not present * @throws {Error} if .error is present the response */ async apiOne (method, params = {}, expectedKey) { const result = await this.api([{ method, params }]); if ( result[0] == null || result[0].error || (expectedKey != null && result[0][expectedKey] == null) ) { const innerObject = result[0]?.error || result; const error = new Error( `Error for api method: "${method}" with params: ${JSON.stringify( params )} >> Result: ${JSON.stringify(innerObject)}"` ); error.innerObject = innerObject; throw error; } if (expectedKey != null) return result[0][expectedKey]; return result[0]; } /** * Revoke : Delete the accessId * - Do not thow error if access is already revoked, just return null; * @param {boolean} [throwOnFail = true] - if set to false do not throw Error on failure * @param {Connection} [usingConnection] - specify which connection issues the revoke, might be necessary when selfRovke */ async revoke (throwOnFail = true, usingConnection) { usingConnection = usingConnection || this; let accessInfo = null; // get accessId try { accessInfo = await this.accessInfo(); } catch (e) { if (e.response?.body?.error?.id === 'invalid-access-token') { return null; // Already revoked OK.. } if (throwOnFail) throw e; return null; } // delete access try { const result = usingConnection.apiOne('accesses.delete', { id: accessInfo.id }); return result; } catch (e) { if (throwOnFail) throw e; return null; } } /** * @private */ async _chunkedBatchCall (arrayOfAPICalls, progress, callHandler) { if (!Array.isArray(arrayOfAPICalls)) { throw new Error('Connection.api() takes an array as input'); } const res = []; let percent = 0; for ( let cursor = 0; arrayOfAPICalls.length >= cursor; cursor += this.options.chunkSize ) { const thisBatch = []; const cursorMax = Math.min( cursor + this.options.chunkSize, arrayOfAPICalls.length ); // copy only method and params into a back call to be exuted for (let i = cursor; i < cursorMax; i++) { thisBatch.push({ method: arrayOfAPICalls[i].method, params: arrayOfAPICalls[i].params }); } const resRequest = await callHandler(thisBatch); // result checks if (!resRequest || !Array.isArray(resRequest.results)) { throw new Error( 'API call result is not an Array: ' + JSON.stringify(resRequest) ); } if (resRequest.results.length !== thisBatch.length) { throw new Error( 'API call result Array does not match request: ' + JSON.stringify(resRequest) ); } // eventually call handleResult for (let i = 0; i < resRequest.results.length; i++) { if (arrayOfAPICalls[i + cursor].handleResult) { await arrayOfAPICalls[i + cursor].handleResult.call( null, resRequest.results[i] ); } } Array.prototype.push.apply(res, resRequest.results); percent = Math.round((100 * res.length) / arrayOfAPICalls.length); if (progress) { progress(percent, res); } } return res; } /** * Post to API return results * @param {(Array | Object)} data * @param {Object} queryParams * @param {string} path * @returns {Promise<Array|Object>} Promise to result.body */ async post (path, data, queryParams) { const now = getTimestamp(); const res = await this.postRaw(path, data, queryParams); this._handleMeta(res.body, now); return res.body; } /** * Raw Post to API return superagent object * @param {Array | Object} data * @param {Object} queryParams * @param {string} path * @returns {request.superagent} Promise from superagent's post request */ async postRaw (path, data, queryParams) { return this._post(path).query(queryParams).send(data); } _post (path) { return utils.superagent .post(this.endpoint + path) .set('Authorization', this.token) .set('accept', 'json'); } /** * Post to API return results * @param {Object} queryParams * @param {string} path * @returns {Promise<Array|Object>} Promise to result.body */ async get (path, queryParams) { const now = getTimestamp(); const res = await this.getRaw(path, queryParams); this._handleMeta(res.body, now); return res.body; } /** * Raw Get to API return superagent object * @param {Object} queryParams * @param {string} path * @returns {request.superagent} Promise from superagent's get request */ getRaw (path, queryParams) { path = path || ''; return utils.superagent .get(this.endpoint + path) .set('Authorization', this.token) .set('accept', 'json') .query(queryParams); } /** * ADD Data Points to HFEvent (flatJSON format) * https://api.pryv.com/reference/#add-hf-series-data-points */ async addPointsToHFEvent (eventId, fields, points) { const res = await this.post('events/' + eventId + '/series', { format: 'flatJSON', fields: fields, points: points }); if (!res.status === 'ok') { throw new Error('Failed loading serie: ' + JSON.stringify(res.status)); } return res; } /** * Streamed get Event. * Fallbacks to not streamed, for browsers that does not support `fetch()` API * @see https://api.pryv.com/reference/#get-events * @param {Object} queryParams See `events.get` parameters * @param {Function} forEachEvent Function taking one event as parameter. Will be called for each event * @returns {Promise<Object>} Promise to result.body transformed with `eventsCount: {count}` replacing `events: [...]` */ async getEventsStreamed (queryParams, forEachEvent) { const myParser = jsonParser(forEachEvent, queryParams.includeDeletions); let res = null; if (typeof window === 'undefined') { // node res = await this.getRaw('events', queryParams) .buffer(false) .parse(myParser); } else if ( typeof fetch !== 'undefined' && !(typeof navigator !== 'undefined' && navigator.product === 'ReactNative') ) { // browser supports fetch and it is not react native res = await browserGetEventStreamed(this, queryParams, myParser); } else { // browser no fetch supports console.log( 'WARNING: Browser does not support fetch() required by pryv.Connection.getEventsStreamed()' ); res = await this.getRaw('events', queryParams); res.body.eventsCount = 0; if (res.body.events) { res.body.events.forEach(forEachEvent); res.body.eventsCount += res.body.events.length; delete res.body.events; } if (res.body.eventDeletions) { // deletions are in a seprated Array res.body.eventDeletions.forEach(forEachEvent); res.body.eventsCount += res.body.eventDeletions.length; delete res.body.eventDeletions; } } const now = getTimestamp(); this._handleMeta(res.body, now); return res.body; } /** * Create an event with attached file * NODE.jS ONLY * @param {Event} event * @param {string} filePath */ async createEventWithFile (event, filePath) { const res = await this._post('events') .field('event', JSON.stringify(event)) .attach('file', filePath); const now = getTimestamp(); this._handleMeta(res.body, now); return res.body; } /** * Create an event from a Buffer * @param {Event} event * @param {Buffer|Blob} bufferData - Buffer for node, Blob for browser * @param {string} fileName */ async createEventWithFileFromBuffer (event, bufferData, filename) { if (typeof window === 'undefined') { // node const res = await this._post('events') .field('event', JSON.stringify(event)) .attach('file', bufferData, filename); const now = getTimestamp(); this._handleMeta(res.body, now); return res.body; } else { /* global FormData */ const formData = new FormData(); formData.append('file', bufferData, filename); const body = await this.createEventWithFormData(event, formData); return body; } } /** * Create an event with attached formData * !! BROWSER ONLY * @param {Event} event * @param {FormData} formData https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData */ async createEventWithFormData (event, formData) { formData.append('event', JSON.stringify(event)); const res = await this._post('events').send(formData); return res.body; } /** * Difference in seconds between the Pryv.io API and local time * deltaTime is refined at each (non-raw) API call * @readonly * @property {number} deltaTime */ get deltaTime () { return this._deltaTime.value; } /** * API endpoint of this connection * @readonly * @property {APIEndpoint} deltaTime */ get apiEndpoint () { return utils.buildAPIEndpoint(this); } // private method that handle meta data parsing _handleMeta (res, requestLocalTimestamp) { if (!res.meta) throw new Error('Cannot find .meta in response.'); if (!res.meta.serverTime) { throw new Error('Cannot find .meta.serverTime in response.'); } // update deltaTime and weight it this._deltaTime.value = (this._deltaTime.value * this._deltaTime.weight + res.meta.serverTime - requestLocalTimestamp) / ++this._deltaTime.weight; } } module.exports = Connection; function getTimestamp () { return Date.now() / 1000; } // service is require "after" to allow circular require const Service = require('./Service'); /** * API Method call, for batch call https://api.pryv.com/reference/#call-batch * @typedef {Object} MethodCall * @property {string} method - The method id * @property {(Object|Array)} params - The call parameters as required by the method. * @property {(Function|Promise)} [handleResult] - Will be called with the result corresponding to this specific call. */