var utility = require('./utility/utility.js'),
ConnectionEvents = require('./connection/ConnectionEvents.js'),
ConnectionStreams = require('./connection/ConnectionStreams.js'),
ConnectionProfile = require('./connection/ConnectionProfile.js'),
ConnectionBookmarks = require('./connection/ConnectionBookmarks.js'),
ConnectionAccesses = require('./connection/ConnectionAccesses.js'),
ConnectionMonitors = require('./connection/ConnectionMonitors.js'),
ConnectionAccount = require('./connection/ConnectionAccount.js'),
CC = require('./connection/ConnectionConstants.js'),
Datastore = require('./Datastore.js'),
_ = require('underscore');
/**
* @class Connection
* Create an instance of Connection to Pryv API.
* The connection will be opened on
* http[s]://<username>.<domain>:<port>/<extraPath>?auth=<auth>
*
* @example
* // create a connection for the user 'pryvtest' with the token 'TTZycvBTiq'
* var conn = new pryv.Connection({username: 'pryvtest', auth: 'TTZycvBTiq'});
*
* @constructor
* @this {Connection}
* @param {Object} [settings]
* @param {string} settings.username
* @param {string} settings.auth - the authorization token for this username
* @param {number} [settings.port = 443]
* @param {string} [settings.domain = 'pryv.me'] change the domain.
* @param {boolean} [settings.ssl = true] Use ssl (https) or no
* @param {string} [settings.extraPath = ''] append to the connections. Must start with a '/'
*/
module.exports = Connection;
function Connection() {
var settings;
if (!arguments[0] || typeof arguments[0] === 'string') {
console.warn('new Connection(username, auth, settings) is deprecated.',
'Please use new Connection(settings)', arguments);
this.username = arguments[0];
this.auth = arguments[1];
settings = arguments[2];
} else {
settings = arguments[0];
this.username = settings.username;
this.auth = settings.auth;
if (settings.url) {
var urlInfo = utility.urls.parseServerURL(settings.url);
this.username = urlInfo.username;
settings.hostname = urlInfo.hostname;
settings.domain = urlInfo.domain;
settings.port = urlInfo.port;
settings.extraPath = urlInfo.path === '/' ? '' : urlInfo.path;
settings.ssl = urlInfo.isSSL();
}
}
this._serialId = Connection._serialCounter++;
this.settings = _.extend({
port: 443,
ssl: true,
extraPath: '',
staging: false
}, settings);
this.settings.domain = settings.domain ?
settings.domain : utility.urls.defaultDomain;
this.serverInfos = {
// nowLocalTime - nowServerTime
deltaTime: null,
apiVersion: null,
lastSeenLT: null,
lastSeenST: null,
};
this._accessInfo = null;
this._privateProfile = null;
this._streamSerialCounter = 0;
this._eventSerialCounter = 0;
/**
* Manipulate events for this connection
* @type {ConnectionEvents}
*/
this.events = new ConnectionEvents(this);
/**
* Manipulate streams for this connection
* @type {ConnectionStreams}
*/
this.streams = new ConnectionStreams(this);
/**
* Manipulate app profile for this connection
* @type {ConnectionProfile}
*/
this.profile = new ConnectionProfile(this);
/**
* Manipulate bookmarks for this connection
* @type {ConnectionProfile}
*/
this.bookmarks = new ConnectionBookmarks(this, Connection);
/**
* Manipulate accesses for this connection
* @type {ConnectionProfile}
*/
this.accesses = new ConnectionAccesses(this);
/**
* Manipulate this connection monitors
*/
this.monitors = new ConnectionMonitors(this);
this.account = new ConnectionAccount(this);
this.datastore = null;
}
Connection._serialCounter = 0;
/**
* In order to access some properties such as event.stream and get a {Stream} object, you
* need to fetch the structure at least once. For now, there is now way to be sure that the
* structure is up to date. Soon we will implement an optional parameter "keepItUpToDate", that
* will do that for you.
*
* TODO implements "keepItUpToDate" logic.
* @param {Streams~getCallback} callback - array of "root" Streams
* @returns {Connection} this
*/
Connection.prototype.fetchStructure = function (callback /*, keepItUpToDate*/) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
if (this.datastore) { return this.datastore.init(callback); }
this.datastore = new Datastore(this);
this.accessInfo(function (error) {
if (error) { return callback(error); }
this.datastore.init(callback);
}.bind(this));
return this;
};
/**
* Set username / auth to this Connection
* @param credentials key / value map containing username and token fields
* @param callback
*/
Connection.prototype.attachCredentials = function (credentials, callback) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
if (!credentials.username || !credentials.auth) {
callback('error: incorrect input parameters');
} else {
this.username = credentials.username;
this.auth = credentials.auth;
callback(null, this);
}
};
/**
* Get access information related this connection. This is also the best way to test
* that the combination username/token is valid.
* @param {Connection~accessInfoCallback} callback
* @returns {Connection} this
*/
Connection.prototype.accessInfo = function (callback) {
if (this._accessInfo) {
return this._accessInfo;
}
this.request({
method: 'GET',
path: '/access-info',
callback: function (error, result) {
if (!error) {
this._accessInfo = result;
}
if (typeof(callback) === 'function') {
return callback(error, result);
}
}.bind(this)
});
return this;
};
/**
* Get the private profile related this connection.
* @param {Connection~privateProfileCallback} callback
* @returns {Connection} this
*/
Connection.prototype.privateProfile = function (callback) {
if (this._privateProfile) {
return this._privateProfile;
}
this.profile.getPrivate(null, function (error, result) {
if (result && result.message) {
error = result;
}
if (!error) {
this._privateProfile = result;
}
if (typeof(callback) === 'function') {
return callback(error, result);
}
}.bind(this));
return this;
};
/**
* Translate this timestamp (server dimension) to local system dimension
* This could have been named to "translate2LocalTime"
* @param {number} serverTime timestamp (server dimension)
* @returns {number} timestamp (local dimension) same time space as (new Date()).getTime();
*/
Connection.prototype.getLocalTime = function (serverTime) {
return (serverTime + this.serverInfos.deltaTime) * 1000;
};
/**
* Translate this timestamp (local system dimension) to server dimension
* This could have been named to "translate2ServerTime"
* @param {number} localTime timestamp (local dimension) same time space as (new Date()).getTime();
* @returns {number} timestamp (server dimension)
*/
Connection.prototype.getServerTime = function (localTime) {
if (typeof localTime === 'undefined') { localTime = new Date().getTime(); }
return (localTime / 1000) - this.serverInfos.deltaTime;
};
// ------------- monitor this connection --------//
/**
* Start monitoring this Connection. Any change that occurs on the connection (add, delete, change)
* will trigger an event. Changes to the filter will also trigger events if they have an impact on
* the monitored data.
* @param {Filter} filter - changes to this filter will be monitored.
* @returns {Monitor}
*/
Connection.prototype.monitor = function (filter) {
return this.monitors.create(filter);
};
// ------------- start / stop Monitoring is called by Monitor constructor / destructor -----//
/**
* Do a direct request to Pryv's API.
* Even if exposed there must be an abstraction for every API call in this library.
* @param {Object} params object with
* @param {string} params.method - GET | POST | PUT | DELETE
* @param {string} params.path - to resource, starting with '/' like '/events'
* @param {Object} params.jsonData - data to POST or PUT
* @param {Boolean} params.isFile indicates if the data is a binary file.
* @params {string} [params.parseResult = 'json'] - 'json|binary'
* @param {Connection~requestCallback} params.callback called when the request is finished
* @param {Connection~requestCallback} params.progressCallback called when the request gives
* progress updates
*/
Connection.prototype.request = function (params) {
if (arguments.length > 1) {
console.warn('Connection.request(method, path, callback, jsonData, isFile, progressCallback)' +
' is deprecated. Please use Connection.request(params).', arguments);
params = {};
params.method = arguments[0];
params.path = arguments[1];
params.callback = arguments[2];
params.jsonData = arguments[3];
params.isFile = arguments[4];
params.progressCallback = arguments[5];
}
if (typeof(params.callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
var headers = { 'authorization': this.auth };
var withoutCredentials = false;
var payload = JSON.stringify({});
if (params.jsonData && !params.isFile) {
payload = JSON.stringify(params.jsonData);
headers['Content-Type'] = 'application/json; charset=utf-8';
}
if (params.isFile) {
payload = params.jsonData;
headers['Content-Type'] = 'multipart/form-data';
headers['X-Requested-With'] = 'XMLHttpRequest';
withoutCredentials = true;
}
var request = utility.request({
method : params.method,
host : getHostname(this),
port : this.settings.port,
ssl : this.settings.ssl,
path : this.settings.extraPath + params.path,
headers : headers,
payload : payload,
progressCallback: params.progressCallback,
//TODO: decide what callback convention to use (Node or jQuery)
success : onSuccess.bind(this),
error : onError.bind(this),
withoutCredentials: withoutCredentials,
parseResult: params.parseResult
});
/**
* @this {Connection}
*/
function onSuccess(data, responseInfo) {
var apiVersion = responseInfo.headers['API-Version'] ||
responseInfo.headers[CC.Api.Headers.ApiVersion];
// test if API is reached or if we headed into something else
if (!apiVersion) {
var error = {
id: CC.Errors.API_UNREACHEABLE,
message: 'Cannot find API-Version',
details: 'Response code: ' + responseInfo.code +
' Headers: ' + JSON.stringify(responseInfo.headers)
};
return params.callback(error, null, responseInfo);
} else if (data.error) {
return params.callback(data.error, null, responseInfo);
}
this.serverInfos.lastSeenLT = (new Date()).getTime();
this.serverInfos.apiVersion = apiVersion || this.serverInfos.apiVersion;
if (_.has(responseInfo.headers, CC.Api.Headers.ServerTime)) {
this.serverInfos.deltaTime = (this.serverInfos.lastSeenLT / 1000) -
responseInfo.headers[CC.Api.Headers.ServerTime];
this.serverInfos.lastSeenST = CC.Api.Headers.ServerTime;
}
if (data && data.meta) {
responseInfo.meta = data.meta;
}
params.callback(null, data, responseInfo);
}
function onError(error, responseInfo) {
var errorTemp = {
id : CC.Errors.API_UNREACHEABLE,
message: 'Error on request ',
details: 'ERROR: ' + error
};
params.callback(errorTemp, null, responseInfo);
}
return request;
};
/**
* @property {string} Connection.id an unique id that contains all needed information to access
* this Pryv data source. http[s]://<username>.<domain>:<port>[/extraPath]/?auth=<auth token>
*/
Object.defineProperty(Connection.prototype, 'id', {
get: function () {
var id = this.settings.ssl ? 'https://' : 'http://';
id += getHostname(this) + ':' +
this.settings.port + this.settings.extraPath + '/?auth=' + this.auth;
return id;
},
set: function () { throw new Error('ConnectionNode.id property is read only'); }
});
/**
* @property {string} Connection.displayId an id easily readable <username>:<access name>
*/
Object.defineProperty(Connection.prototype, 'displayId', {
get: function () {
if (! this._accessInfo) {
throw new Error('connection must have been initialized to use displayId. ' +
' You can call accessInfo() for this');
}
var id = this.username + ':' + this._accessInfo.name;
return id;
},
set: function () { throw new Error('Connection.displayId property is read only'); }
});
/**
* @property {String} Connection.serialId A locally-unique id for the connection; can also be
* used as a client-side id
*/
Object.defineProperty(Connection.prototype, 'serialId', {
get: function () { return 'C' + this._serialId; }
});
/**
* Called with the desired Streams as result.
* @callback Connection~accessInfoCallback
* @param {Object} error - eventual error
* @param {AccessInfo} result
*/
/**
* @typedef AccessInfo
* @see http://api.pryv.com/reference.html#data-structure-access
*/
/**
* Called with the result of the request
* @callback Connection~requestCallback
* @param {Object} error - eventual error
* @param {Object} result - jSonEncoded result
* @param {Object} resultInfo
* @param {Number} resultInfo.code - HTTP result code
* @param {Object} resultInfo.headers - HTTP result headers by key
*/
// --------- login
/**
* Static method to login, returns a connection object in the callback if the username/password
* pair is valid for the provided appId.
*
* @param params key / value map containing username, password and appId fields and optional
* domain and origin fields
* @param callback
*/
Connection.login = function (params, callback) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
var headers = {
'Content-Type': 'application/json'
};
if (!utility.isBrowser()) {
var origin = 'https://sw.';
origin = params.origin ? origin + params.origin :
origin + utility.urls.domains.client.production;
_.extend(headers, {Origin: origin});
}
var domain = params.domain || utility.urls.domains.client.production;
var pack = {
method: 'POST',
headers: headers,
ssl: true,
host: params.username + '.' + domain,
path: '/auth/login/',
payload: JSON.stringify({
appId: params.appId,
username: params.username,
password: params.password
}),
success: function (data, responseInfo) {
if (data.error) {
return callback(data.error, null, responseInfo);
}
var settings = {
username: params.username,
auth: data.token,
domain: domain
// TODO: set staging if in this mode
};
return callback(null, new Connection(settings), responseInfo);
},
error: function (error, responseInfo) {
callback(error, null, responseInfo);
}
};
utility.request(pack);
};
// --------- batch call
/**
* address multiple methods to the API in a single batch call
*
* @example
* // make a batch call to create an event and update a stream
* connection.batchCall(
* [
* { method: 'events.create',
* params: {
* streamId: 'diary',
* type: 'note/txt',
* content: 'hello'
* }
* },
* { method: 'streams.update',
* params: {
* id': 'diary',
* params: {
* update: { name: 'new diary' }
* }
* ], function (err, results) {
* if (err) {
* return console.log(err);
* }
* results.forEach(function (result) {
* console.log(result);
* }
* });
* @param {Array} methodsData - array of methods to execute on the API,
* @param {Function} callback - callback
*/
Connection.prototype.batchCall = function(methodsData, callback) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
if (!_.isArray(methodsData)) { methodsData = [methodsData]; }
this.request({
method: 'POST',
path: '/',
jsonData: methodsData,
callback: function (err, res) {
if (err) {
return callback(err);
}
callback(null, res.results);
}.bind(this)
});
};
/**
* Method to logout the current connection from API, clearing the SSO cookies
* @param callbacks: set of callbacks, in particular:
* callbacks.error: function to be called in case of logout error
* callbacks.signedOut: function to be called after a successful logout
*/
Connection.prototype.trustedLogout = function(callbacks) {
this.request({
method: 'POST',
path: '/auth/logout',
callback: function (error) {
if (error && typeof(callbacks.error) === 'function') {
return callbacks.error(error);
} else if (!error && typeof(callbacks.signedOut) === 'function') {
return callbacks.signedOut(this);
}
}.bind(this)
});
};
// --------- private utils
function getHostname(connection) {
return connection.settings.hostname ||
connection.username ?
connection.username + '.' + connection.settings.domain : connection.settings.domain;
}