var utility = require('../utility/utility.js'),
_ = require('underscore'),
Filter = require('../Filter'),
Event = require('../Event'),
CC = require('./ConnectionConstants.js');
/**
* @class ConnectionEvents
*
* Coverage of the API
* GET /events -- 100%
* POST /events -- only data (no object)
* POST /events/start -- 0%
* POST /events/stop -- 0%
* PUT /events/{event-id} -- 100%
* DELETE /events/{event-id} -- only data (no object)
* POST /events/batch -- only data (no object)
*
* attached files manipulations are covered by Event
*
*
* @param {Connection} connection
* @constructor
*/
function ConnectionEvents(connection) {
this.connection = connection;
}
/**
* @example
* // get events from the Diary stream
* conn.events.get({streamId : 'diary'},
* function(events) {
* console.log('got ' + events.length + ' events)
* }
* );
* @param {FilterLike} filter
* @param {ConnectionEvents~getCallback} doneCallback
* @param {ConnectionEvents~partialResultCallback} partialResultCallback
*/
ConnectionEvents.prototype.get = function (filter, doneCallback, partialResultCallback) {
if (typeof(doneCallback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
//TODO handle caching
var result = [];
filter = filter || {};
this._get(filter, function (error, res, extraInfos) {
if (error) {
result = null;
} else {
var eventList = res.events || res.event;
_.each(eventList, function (eventData) {
var event = null;
if (!this.connection.datastore) { // no datastore break
event = new Event(this.connection, eventData);
} else {
event = this.connection.datastore.createOrReuseEvent(eventData);
}
result.push(event);
}.bind(this));
if (res.eventDeletions) {
result.eventDeletions = res.eventDeletions;
}
}
doneCallback(error, result, extraInfos);
if (partialResultCallback) { partialResultCallback(result); }
}.bind(this));
};
/**
* @param {Event} event
* @param {Connection~requestCallback} callback
*/
ConnectionEvents.prototype.update = function (event, callback) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
this._updateWithIdAndData(event.id, event.getData(), callback);
};
/**
* @param {Event | eventId} event
* @param {Connection~requestCallback} callback
*/
ConnectionEvents.prototype.delete = ConnectionEvents.prototype.trash = function (event, callback) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
this.trashWithId(event.id, callback);
};
/**
* @param {String} eventId
* @param {Connection~requestCallback} callback
*/
ConnectionEvents.prototype.trashWithId = function (eventId, callback) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
this.connection.request({
method: 'DELETE',
path: '/events/' + eventId,
callback: function (error, result) {
// assume there is only one event (no batch for now)
if (result && result.event) {
if (!this.connection.datastore) { // no datastore break
result = new Event(this.connection, result.event);
} else {
result = this.connection.datastore.createOrReuseEvent(result.event);
}
} else {
result = null;
}
callback(error, result);
}.bind(this)
});
};
/**
* This is the preferred method to create an event, or to create it on the API.
* The function return the newly created object.. It will be updated when posted on the API.
* @param {NewEventLike} event -- minimum {streamId, type } -- if typeof Event, must belong to
* the same connection and not exists on the API.
* @param {ConnectionEvents~eventCreatedOnTheAPI} callback
* @param {Boolean} [start = false] if set to true will POST the event to /events/start
* @return {Event} event
*/
ConnectionEvents.prototype.create = function (newEventlike, callback) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
_create.call(this, newEventlike, callback, false);
};
/**
* This is the preferred method to create and start an event, Starts a new period event.
* This is equivalent to starting an event with a null duration. In singleActivity streams,
* also stops the previously running period event if any.
* @param {NewEventLike} event -- minimum {streamId, type } -- if typeof Event, must belong to
* the same connection and not exists on the API.
* @param {ConnectionEvents~eventCreatedOnTheAPI} callback
* @return {Event} event
*/
ConnectionEvents.prototype.start = function (newEventlike, callback) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
_create.call(this, newEventlike, callback, true);
};
// common call for create and start
function _create(newEventlike, callback, start) {
var event = null;
if (newEventlike instanceof Event) {
if (newEventlike.connection !== this.connection) {
return callback(new Error('event.connection does not match current connection'));
}
if (newEventlike.id) {
return callback(new Error('cannot create an event already existing on the API'));
}
event = newEventlike;
} else {
event = new Event(this.connection, newEventlike);
}
var url = '/events';
if (start) { url = '/events/start'; }
this.connection.request({
method: 'POST',
path: url,
jsonData: event.getData(),
callback: function (err, result, resultInfo) {
if (!err && resultInfo.code !== 201) {
err = {id: CC.Errors.INVALID_RESULT_CODE};
}
/**
* Change will happend with offline caching...
*
* An error may hapend 400.. or other if app is behind an non-opened gateway. Thus making
* difficult to detect if the error is a real bad request.
* The first step would be to consider only bad request if the response can be identified
* as coming from a valid api-server. If not, we should cache the event for later synch
* then remove the error and send the cached version of the event.
*
*/
// TODO if err === API_UNREACHABLE then save event in cache
if (result && !err) {
_.extend(event, result.event);
if (this.connection.datastore) { // if datastore is activated register new event
this.connection.datastore.addEvent(event);
}
}
callback(err, err ? null : event, err ? null : result.stoppedId);
}.bind(this)
});
return event;
}
/**
* Stop an event by it's Id
* @param {EventLike} event -- minimum {id} -- if typeof Event, must belong to
* the same connection and not exists on the API.
* @param {Date} [date = now] the date to set to stop the event
* @param {ConnectionEvents~eventStoppedOnTheAPI} callback
* @return {Event} event
*/
ConnectionEvents.prototype.stopEvent = function (eventlike, date, callback) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
var data = {id: eventlike.id};
if (date) {
data.time = date.getTime() / 1000;
}
this.connection.request({
method: 'POST',
path: '/events/stop',
jsonData: data,
callback: function (err, result, resultInfo) {
if (!err && resultInfo.code !== 200) {
err = {id: CC.Errors.INVALID_RESULT_CODE};
}
// TODO if err === API_UNREACHABLE then save event in cache
/*
if (result && ! err) {
if (this.connection.datastore) { // if datastore is activated register new event
}
} */
callback(err, err ? null : result.stoppedId);
}.bind(this)
});
};
/**
* Stop any event in this stream
* @param {StreamLike} stream -- minimum {id} -- if typeof Stream, must belong to
* the same connection and not exists on the API.
* @param {Date} [date = now] the date to set to stop the event
* @param {String} [type = null] stop any matching eventType is this stream.
* @param {ConnectionEvents~eventStoppedOnTheAPI} callback
* @return {Event} event
*/
ConnectionEvents.prototype.stopStream = function (streamLike, date, type, callback) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
var data = {streamId : streamLike.id };
if (date) { data.time = date.getTime() / 1000; }
if (type) { data.type = type; }
this.connection.request({
method: 'POST',
path: '/events/stop',
jsonData: data,
callback: function (err, result, resultInfo) {
if (!err && resultInfo.code !== 200) {
err = {id: CC.Errors.INVALID_RESULT_CODE};
}
// TODO if err === API_UNREACHABLE then cache the stop instruction for later synch
callback(err, err ? null : result.stoppedId);
}.bind(this)
});
};
/**
* @param {NewEventLike} event -- minimum {streamId, type } -- if typeof Event, must belong to
* the same connection and not exists on the API.
* @param {ConnectionEvents~eventCreatedOnTheAPI} callback
* @param {FormData} the formData to post for fileUpload. On node.js
* refers to pryv.utility.forgeFormData
* @return {Event} event
*/
ConnectionEvents.prototype.createWithAttachment =
function (newEventLike, formData, callback, progressCallback) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
var event = null;
if (newEventLike instanceof Event) {
if (newEventLike.connection !== this.connection) {
return callback(new Error('event.connection does not match current connection'));
}
if (newEventLike.id) {
return callback(new Error('cannot create an event already existing on the API'));
}
event = newEventLike;
} else {
event = new Event(this.connection, newEventLike);
}
formData.append('event', JSON.stringify(event.getData()));
this.connection.request({
method: 'POST',
path: '/events',
jsonData: formData,
isFile: true,
progressCallback: progressCallback,
callback: function (err, result) {
if (result) {
_.extend(event, result.event);
if (this.connection.datastore) { // if datastore is activated register new event
this.connection.datastore.addEvent(event);
}
}
callback(err, event);
}.bind(this)
});
};
/**
* @param {String} eventId
* @param {ConnectionEvents~eventCreatedOnTheAPI} callback
* @param {FormData} the formData to post for fileUpload. On node.js
* refers to pryv.utility.forgeFormData
* @return {Event} event
*/
ConnectionEvents.prototype.addAttachment =
function (eventId, formData, callback, progressCallback) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
this.connection.request({
method: 'POST',
path: '/events/' + eventId,
jsonData: formData,
isFile: true,
progressCallback: progressCallback,
callback: function (err, result) {
if (err) {
return callback(err);
}
callback(null, result.event);
}
});
};
/**
* @param {String} eventId
* @param {ConnectionEvents~eventCreatedOnTheAPI} callback
* @param {FormData} the formData to post for fileUpload. On node.js
* refers to pryv.utility.forgeFormData
* @return {Event} event
*/
ConnectionEvents.prototype.getAttachment =
function (params, callback, progressCallback) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
this.connection.request({
method: 'GET',
path: '/events/' + params.eventId + '/' + params.fileId,
progressCallback: progressCallback,
parseResult: 'binary',
callback: function (err, result) {
if (err) {
return callback(err);
}
callback(null, result);
}
});
};
/**
* @param {String} eventId
* @param {ConnectionEvents~eventCreatedOnTheAPI} callback
* @param {String} fileName
* @return {Event} event
*/
ConnectionEvents.prototype.removeAttachment = function (eventId, fileName, callback) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
this.connection.request({
method: 'DELETE',
path: '/events/' + eventId + '/' + fileName,
callback: callback
});
};
/**
* //TODO rename to batch
* //TODO make it NewEventLike compatible
* //TODO once it support an array of mixed values Event and EventLike, the, no need for
* callBackWithEventsBeforeRequest at it will. A dev who want Event object just have to create
* them before
* This is the prefered method to create events in batch
* @param {Object[]} eventsData -- minimum {streamId, type }
* @param {ConnectionEvents~eventBatchCreatedOnTheAPI}
* @param {function} [callBackWithEventsBeforeRequest] mostly for testing purposes
* @return {Event[]} events
*/
ConnectionEvents.prototype.batchWithData =
function (eventsData, callback, callBackWithEventsBeforeRequest) {
if (typeof(callback) !== 'function') {
throw new Error(CC.Errors.CALLBACK_IS_NOT_A_FUNCTION);
}
if (!_.isArray(eventsData)) { eventsData = [eventsData]; }
var createdEvents = [];
var eventMap = {};
// use the serialId as a temporary Id for the batch
_.each(eventsData, function (eventData, i) {
var event = new Event(this.connection, eventData);
createdEvents.push(event);
eventMap[i] = event;
}.bind(this));
if (callBackWithEventsBeforeRequest) {
callBackWithEventsBeforeRequest(createdEvents);
}
var mapBeforePush = function (evs) {
return _.map(evs, function (e) {
return {
method: 'events.create',
params: e
};
});
};
this.connection.request({
method: 'POST',
path: '/',
jsonData: mapBeforePush(eventsData),
callback: function (err, result) {
if (!err && result) {
_.each(result.results, function (eventData, i) {
_.extend(eventMap[i], eventData.event); // add the data to the event
if (this.connection.datastore) { // if datastore is activated register new event
this.connection.datastore.addEvent(eventMap[i]);
}
}.bind(this));
}
callback(err, createdEvents);
}.bind(this)
});
return createdEvents;
};
// --- raw access to the API
/**
* TODO anonymise by renaming to function _get(..
* @param {FilterLike} filter
* @param {Connection~requestCallback} callback
* @private
*/
ConnectionEvents.prototype._get = function (filter, callback) {
var tParams = filter;
if (filter instanceof Filter) { tParams = filter.getData(true); }
if (_.has(tParams, 'streams') && tParams.streams.length === 0) { // dead end filter..
return callback(null, [], {});
}
this.connection.request({
method: 'GET',
path: '/events?' + utility.getQueryParametersString(tParams),
callback: callback
});
};
/**
* TODO anonymise by renaming to function _xx(..
* @param {String} eventId
* @param {Object} data
* @param {Connection~requestCallback} callback
* @private
*/
ConnectionEvents.prototype._updateWithIdAndData = function (eventId, data, callback) {
this.connection.request({
method: 'PUT',
path: '/events/' + eventId,
jsonData: data,
callback: function (error, result) {
if (!error && result && result.event) {
if (!this.connection.datastore) {
result = new Event(this.connection, result.event);
} else {
result = this.connection.datastore.createOrReuseEvent(result.event);
}
} else {
result = null;
}
callback(error, result);
}.bind(this)
});
};
/**
* @private
* @param {Event} event
* @param {Object} the data to map
*/
ConnectionEvents.prototype._registerNewEvent = function (event, data) {
if (! event.connection.datastore) { // no datastore break
_.extend(event, data);
return event;
}
return event.connection.datastore.createOrReuseEvent(this, data);
};
module.exports = ConnectionEvents;
/**
* Called with the desired Events as result.
* @callback ConnectionEvents~getCallback
* @param {Object} error - eventual error
* @param {Event[]} result
*/
/**
* Called each time a "part" of the result is received
* @callback ConnectionEvents~partialResultCallback
* @param {Event[]} result
*/
/**
* Called when an event is created on the API
* @callback ConnectionEvents~eventCreatedOnTheAPI
* @param {Object} error - eventual error
* @param {Event} event
*/
/**
* Called when an event is created on the API
* @callback ConnectionEvents~eventStoppedOnTheAPI
* @param {Object} error - eventual error
* @param {String} stoppedEventId or null if event not found
*/
/**
* Called when batch create an array of events on the API
* @callback ConnectionEvents~eventBatchCreatedOnTheAPI
* @param {Object} error - eventual error
* @param {Event[]} events
*/