@janiscommerce/app-tracking-time
Version:
A package for managing the times of actions in applications
401 lines (328 loc) • 13.4 kB
JavaScript
import Event from './event';
import EventTrackerError from './event-tracker-error';
import Database from './database';
import Validations from '../utils/validations';
import Helpers from '../utils/helpers';
import {differenceInMilliseconds} from 'date-fns';
/**
* Manages events, including adding, retrieving, and validating them.
*
* @class
* @description The `EventTracker` class provides functionality for managing events, including adding new events,
* validating event sequences, and retrieving event data. It interacts with the `Database` class to
* perform operations on the event records.
*/
class EventTracker {
/**
* @param {string} filename - The name of the database file used by the `EventTracker` instance.
*/
constructor(filename) {
this.db = new Database(filename);
}
/**
* Adds an event to the database after performing validations.
*
* @async
* @name addEvent
* @param {{id: string, type: string, time: string, payload: object}} params
* @param {string} params.id
* @param {"start"|"pause"|"resume"|"finish"} params.type
* @param {string} params.time current time in isoString format
* @param {object} params.payload any data that you want to save associated with the id and type
* @returns {Promise<{id: string, time: number}>} A promise that resolves to an object containing the `id` of the event and the `time` the event was created.
* @throws {Error} If any validation fails or the event cannot be saved to the database.
*/
async addEvent(params) {
try {
const {id, type, time, payload} = params;
await Validations.idValidation(id);
await this._eventValidation(id, type);
const createdEvent = Event.create(id, type, time, payload);
await this.db.save(createdEvent);
return {
id,
time: createdEvent.time,
};
} catch (error) {
return Promise.reject(error);
}
}
/**
* @name getEventsById
* @description This method allows you to obtain all the events related to the id
* @param {string} id
* @returns {Promise<{id: string, time: string, payload: object, type: string}[]}>}
*/
async getEventsById(id) {
try {
await Validations.idValidation(id);
const filters = Helpers.getFilters({id});
const events = await this.db.search(filters, id);
return events.map((e) => Event.parseEventFromDB(e));
} catch (error) {
return Promise.reject(error);
}
}
/**
* Retrieves the last event associated with a given ID.
*
* @name getLastEventById
* @param {string} id - The ID for which to retrieve the last event type.
* @returns {Promise<{id: string, time: string, payload: object, type: string}>} A promise that resolves to the last event, or an empty string if no event is found.
* @throws {Error} If the ID is invalid or an error occurs during the search process.
*/
async getLastEventById(id) {
try {
await Validations.idValidation(id);
const filters = Helpers.getFilters({id});
const events = await this.db.search(filters, id);
let registerEvents = Helpers.reverseArray(events);
const findedEvent = registerEvents[0];
if (!findedEvent) return {};
return Event.parseEventFromDB(findedEvent);
} catch (error) {
return Promise.reject(error);
}
}
/**
* Calculates the elapsed time between the start and finish events.
* When not receive finish time, it calculates the difference with the current time
*
* @name getElapsedTime
* @param {{startTime, finishTime, format}} param
* @param {string} startTime start time in iso string format
* @param {string} finishTime finish time in iso string format
* @param {boolean} format if true, returns the time in days-hours-minutes-seconds format, otherwise returns only the milliseconds
* @returns {{days: number, hours: number ,minutes: number, seconds: number}} formated elapsed time
*/
getElapsedTime({startTime, finishTime, format = true}) {
if (!startTime)
return format
? {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
}
: 0;
const lastTime = !!finishTime ? finishTime : new Date().toISOString();
return Helpers.getTimeDifference(startTime, lastTime, format);
}
/**
* Calculates the time elapsed between each pause and resume of an id.
*
* @name getStoppedTime
* @param {{id: string, time: string, payload: object, type: string}[]} events events associated with an id
* @param {boolean} format if true, returns the time in days-hours-minutes-seconds format, otherwise returns only the milliseconds
* @returns {{days: number, hours: number ,minutes: number, seconds: number}} formated stopped time
*/
//istanbul ignore next
getStoppedTime({events = [], format = false}) {
if (!events?.length || !Helpers.isArray(events)) return 0;
const stoppedTime = events.reduce((totalPausedTime, currentEvent, index) => {
if (currentEvent.type !== 'pause') return totalPausedTime;
let nextEvent = events[index + 1] || {};
if (nextEvent.type === 'pause') return totalPausedTime;
if (nextEvent.type !== 'resume' && nextEvent.type !== 'finish') {
nextEvent.time = new Date();
}
const resumeTime = new Date(nextEvent.time);
const pauseTime = new Date(currentEvent.time);
const timeDifference = differenceInMilliseconds(resumeTime, pauseTime);
return totalPausedTime + timeDifference;
}, 0);
if (format) return Helpers.convertMillisecondsToTime(stoppedTime);
return stoppedTime;
}
/**
* Calculates the net time between start and finish, discounting pauses
*
* @name getNetTrackingTime
* @param {string} events tracked events
* @param {boolean} format if true, returns the time in days-hours-minutes-seconds format, otherwise returns only the milliseconds
* @returns {{days: number, hours: number ,minutes: number, seconds: number}} formated net time
*/
getNetTrackingTime({events = [], format = false}) {
if (!events?.length || !Helpers.isArray(events)) return 0;
const startEvent = Helpers.findEventByStatus(events, 'start');
const finishEvent = Helpers.findEventByStatus(events, 'finish');
const elapsedTime = this.getElapsedTime({
startTime: startEvent?.time,
finishTime: finishEvent?.time,
format: false,
});
if (!elapsedTime || elapsedTime <= 0) return 0;
const stoppedTime = this.getStoppedTime({events});
const netTime = elapsedTime - stoppedTime;
if (format) return Helpers.convertMillisecondsToTime(netTime);
return netTime;
}
/**
* Checks if an event with the given ID has already started.
*
* @async
* @name isEventStarted
* @description This method searches for a "start" event associated with the provided ID.
* If a start event is found, it returns `true`, indicating the event has already started.
* If no start event is found, it returns `false`.
* @param {string} id - The ID of the event to check.
* @returns {Promise<boolean>} A promise that resolves to `true` if a start event exists, or `false` if not.
* @throws {EventTrackerError} If the ID is invalid.
* @throws {Error} If an error occurs during the search process.
*/
async isEventStarted(id) {
try {
await Validations.idValidation(id);
const filters = Helpers.getFilters({id, type: 'start'});
const startEvents = await this.db.search(filters, id, 'start');
if (!startEvents.length) return false;
return true;
} catch (error) {
return Promise.reject(error);
}
}
/**
* Generic search method that can be applied to any situation.
* Allows searching events with custom queries and parameters.
*
* @async
* @name searchEventByQuery
* @description This method provides a flexible way to search events using custom queries and parameters.
* It delegates the search operation to the database layer while providing error handling.
* @param {string} query - The search query/filter to apply. Can be empty for all events.
* @param {...any} values - Variable number of values to use in the query (for parameterized queries).
* @returns {Promise<Array>} A promise that resolves to an array of matching events.
* @throws {EventTrackerError} If the database filename is not specified or other database errors occur.
* @example
* // Search all events
* const allEvents = await eventTracker.searchEventByQuery();
*
* // Search by ID
* const eventsById = await eventTracker.searchEventByQuery('id == $0', 'user123');
*
* // Search by type
* const startEvents = await eventTracker.searchEventByQuery('type == $0', 'start');
*
* // Complex query
* const recentEvents = await eventTracker.searchEventByQuery(
* 'id == $0 && type == $1 && time >= $2',
* 'user123', 'start', new Date('2024-01-01')
* );
*/
async searchEventByQuery(query = '', ...values) {
try {
const events = await this.db.search(query, ...values);
return events.map((e) => Event.parseEventFromDB(e));
} catch (error) {
return Promise.reject(error);
}
}
/**
* Deletes all events associated with the given ID from the database.
*
* @async
* @name deleteEventsById
* @description This method deletes all events related to the specified ID by applying filters to the database query.
* @param {string} id - The ID of the events to be deleted.
* @returns {Promise<void>} A promise that resolves when the events are successfully deleted.
* @throws {EventTrackerError} If the ID is invalid.
* @throws {Error} If an error occurs during the deletion process.
*/
async deleteEventsById(id) {
try {
await Validations.idValidation(id);
const filters = Helpers.getFilters({id});
await this.db.delete(filters, id);
} catch (error) {
return Promise.reject(error);
}
}
/**
* Delete all records from the database
* @name deleteAllEvents
* @returns {Promise<void>} A promise that resolves when the events are successfully deleted.
*/
async deleteAllEvents() {
try {
await this.db.deleteAll();
} catch (error) {
return Promise.reject(error);
}
}
/**
* Asynchronously removes a record with the specified `id` and type 'finish' from the database.
*
* @param {string} id - The unique identifier of the record to be removed.
* @returns {Promise<boolean>} - A promise that resolves to `true` if the deletion was successful.
* @throws {Error} - If an error occurs during the deletion process, the promise is rejected with the error.
*/
async removeFinishById(id) {
try {
const dbFilters = 'id LIKE[c] $0 && type = $1';
return await this.db.delete(dbFilters, String(id), 'finish');
} catch (error) {
return Promise.reject(error);
}
}
/**
* Validates the event type and checks if it follows the correct sequence for the given ID.
*
* @async
* @private
* @name _eventValidation
* @description This method first validates the event type to ensure it is valid. It then retrieves the type of the last event
* associated with the provided ID and checks if the new event type follows the correct sequence.
* @param {string} id - The ID of the event to validate.
* @param {string} type - The type of the new event to validate.
* @returns {Promise<boolean>} A promise that resolves to `true` if the event type is valid and follows the correct sequence,
* or `false` otherwise.
* @throws {EventTrackerError} If the event type is invalid or if the event sequence is incorrect.
* @throws {Error} If an error occurs during the validation process.
*/
async _eventValidation(id, type) {
if (!Validations.isValidEventType(type)) throw new EventTrackerError('Event type is invalid');
const {type: previousType} = await this.getLastEventById(id);
return Validations.validateEventsSequence(type, previousType);
}
/**
* Retrieves the `time` property of the last event matching the specified ID and type.
*
* @async
* @param {string|number} id - The identifier used to filter events.
* @param {string} type - The type used to filter events.
* @returns {Promise<number|null>} Resolves with the `time` property of the last matching event, or `null` if no valid event is found.
* @throws {Error} If an error occurs during the search process, the promise is rejected with the error.
*/
async getIdTimeByType(id, type) {
try {
await Validations.idValidation(id);
const isValidType = Validations.isValidEventType(type);
if (!isValidType) throw new EventTrackerError('Event type is invalid');
const filters = Helpers.getFilters({id, type});
const filteredEvents = await this.db.search(filters, id, type);
const lastIndex = filteredEvents.length - 1;
const event = filteredEvents[lastIndex];
const parsedEvent = Event.parseEventFromDB(event);
if (!Helpers.isObject(parsedEvent) || !parsedEvent['time']) return null;
return parsedEvent['time'];
} catch (error) {
return Promise.reject(error);
}
}
/**
* Deletes the database folder and updates the folder existence flag.
*
* @async
* @returns {Promise<void>} Resolves when the database folder is successfully removed.
* @throws {Error} If an error occurs during the folder removal process, the promise is rejected with the error.
*/
async removeEventsFolder() {
try {
await this.db.removeDatabaseFolder();
return true;
} catch (error) {
return Promise.reject(error);
}
}
}
export default EventTracker;