UNPKG

@alithya-oss/backstage-plugin-time-saver-backend

Version:

This plugin provides an implementation of charts and statistics related to your time savings that are coming from usage of your templates. Plugins is built from frontend and backend part. Backend plugin is responsible for scheduled stats parsing process a

575 lines (571 loc) 27.6 kB
'use strict'; var backendPluginApi = require('@backstage/backend-plugin-api'); var mappers = require('./mappers.cjs.js'); const TIME_SAVINGS_TABLE = "ts_template_time_savings"; const migrationsDir = backendPluginApi.resolvePackagePath( "@alithya-oss/backstage-plugin-time-saver-backend", "migrations" ); class TimeSaverDatabase { /** * Constructor for initializing a new instance with a Knex database connection and a logger service. * @param {Knex} db - The Knex database connection. * @param {LoggerService} logger - The logger service for logging. */ constructor(db, logger) { this.db = db; this.logger = logger; } /** * Creates a new instance of TimeSaverStore by initializing a database connection and running migrations if necessary. * * This static method creates a new TimeSaverDatabase instance by obtaining a database client, checking for migrations, running the latest migrations, and returning the initialized TimeSaverDatabase. * * @async * @param {DatabaseService} database - The DatabaseService instance for database operations. * @param {LoggerService} logger - The LoggerService instance for logging. * @returns {Promise<TimeSaverStore>} A promise that resolves to a TimeSaverStore instance. */ static async create(database, logger) { const knex = await database.getClient(); if (!database.migrations?.skip) { await knex.migrate.latest({ directory: migrationsDir }); } return new TimeSaverDatabase(knex, logger); } /** * Logs a success message and returns the result. * * This method logs a success message using the provided logger, stringify the result, and returns the result. * * @template T * @param {T | undefined} result - The result to be logged and returned. * @param {string} [logMessage='Data selected successfully'] - The message to log along with the result. * @returns {T | undefined} The result passed as input. */ ok(result, logMessage = "Data selected successfully") { this.logger.debug(`${logMessage} ${JSON.stringify(result)}`); return result; } /** * Logs an error message, throws the error, and does not return a value. * * This method logs an error message using the provided logger, throws the error passed as input, and does not return a value. * * @param {Error | unknown} error - The error object to log and throw. * @param {string} [errorMessage='Error selecting data:'] - The message to log as an error. */ fail(error, errorMessage = "Error selecting data:", origin = "") { this.logger.error( `${origin !== "" ? `[${origin}] - ` : ""}${errorMessage}`, error ? error : undefined ); throw error; } /** * Formats a date column based on the database client type. * * This method formats a date column according to the specific syntax of the database client type (SQLite, MySQL, MSSQL, or PostgreSQL). * * @param {Knex} knex - The Knex instance for database operations. * @param {string} column - The name of the column to format as a date. * @param {string} [alias='date'] - The alias to use for the formatted date column. * @returns {Knex.Raw} A Knex raw query object representing the formatted date column. */ formatDate(knex, column, alias = "date") { const { client } = knex.client.config; if (client === "better-sqlite3") { return knex.raw(`strftime('%Y-%m-%d', ${column}) as ${alias}`); } else if (client === "mysql" || client === "mysql2") { return knex.raw(`DATE_FORMAT(${column}, '%Y-%m-%d')`); } else if (client === "mssql") { return knex.raw(`FORMAT(${column}, 'yyyy-MM-dd')`); } return knex.raw(`TO_CHAR("${column}", 'YYYY-MM-DD') AS "${alias}"`); } formatGroupBy(knex, column) { const { client } = knex.client.config; if (client === "better-sqlite3") { return `strftime('%Y-%m-%d', "${column}")`; } else if (client === "mysql" || client === "mysql2") { return `DATE_FORMAT(${column}, '%Y-%m-%d')`; } else if (client === "mssql") { return `FORMAT(${column}, 'yyyy-MM-dd')`; } return `TO_CHAR("${column}", 'YYYY-MM-DD')`; } /** * Inserts a TemplateTimeSavings record into the database. * * This method inserts a TemplateTimeSavings record into the specified table, converts the inserted rows to DTO format, and returns the result. * * @param {NonNullable<TemplateTimeSavings>} templateTimeSavings - The TemplateTimeSavings object to insert. * @returns {Promise<TemplateTimeSavings | undefined | void>} A promise resolving to the inserted TemplateTimeSavings record or undefined. */ async insert(templateTimeSavings) { this.logger.debug( `templateTimeSavings insert: ${JSON.stringify(templateTimeSavings)}` ); try { const insertedRows = await this.db( TIME_SAVINGS_TABLE ).insert(mappers.TemplateTimeSavingsMap.toPersistence(templateTimeSavings)).returning("*"); this.logger.debug(`insertedRows: ${JSON.stringify(insertedRows)}`); return this.ok( insertedRows && insertedRows.length ? mappers.TemplateTimeSavingsMap.toDTO(insertedRows[0]) : void 0, "Data inserted successfully" ); } catch (error) { return this.fail(error, "Error inserting data:"); } } /** * Updates a TemplateTimeSavings record in the database based on the provided key. * * This method updates a TemplateTimeSavings record in the specified table using the given key, converts the updated result to DTO format, and returns the updated data. * * @param {TemplateTimeSavings} data - The TemplateTimeSavings object with updated data. * @param {Record<string, string>} key - The key to identify the record to update. * @returns {Promise<TemplateTimeSavings | undefined | void>} A promise resolving to the updated TemplateTimeSavings record or undefined. */ async update(data, key) { this.logger.debug(`templateTimeSavings update: ${JSON.stringify(data)}`); try { const result = await this.db(TIME_SAVINGS_TABLE).where(key).update(mappers.TemplateTimeSavingsMap.toPersistence(data)).returning("*"); this.logger.debug(`updatedRows: ${JSON.stringify(result)}`); return this.ok( result.length > 0 ? mappers.TemplateTimeSavingsMap.toDTO(result[0]) : void 0, "Data updated successfully" ); } catch (error) { return this.fail(error, "Error updating data:"); } } /** * Deletes a record from the TIME_SAVINGS_TABLE based on the specified key. * * This asynchronous method attempts to delete a record that matches the provided key from the database. * If the database client is 'better-sqlite3', it first retrieves the existing record before performing the deletion. * The method returns the deleted record(s) as a DTO array, undefined if no records were found, or void if an error occurs. * * @param {Record<string, string>} key - An object representing the key used to identify the record to be deleted. * @returns {Promise<TemplateTimeSavings[] | undefined | void>} A promise that resolves to an array of deleted records as DTOs, * or undefined if no records were deleted, or void if an error occurs during the deletion process. * @throws {Error} Throws an error if the deletion operation fails. */ async delete(key) { try { const { client } = this.db.client.config; let templateTimeSavings; if (client === "better-sqlite3") { templateTimeSavings = await this.db( TIME_SAVINGS_TABLE ).where(key).select(); } const result = await this.db(TIME_SAVINGS_TABLE).returning("*").where(key).del(); const deletedValue = typeof result === "number" ? templateTimeSavings : result; return this.ok( deletedValue ? mappers.TemplateTimeSavingsCollectionMap.toDTO(deletedValue) : void 0, "Row deleted successfully" ); } catch (error) { return this.fail(error, "Error deleting data. "); } } /** * Truncates the TIME_SAVINGS_TABLE, removing all records. * * This asynchronous method clears all entries from the specified table in the database. * It returns a success response if the truncation is successful, or an error response if the operation fails. * * @returns {Promise<boolean | void>} A promise that resolves to true if the table was truncated successfully, * or void if an error occurs during the truncation process. * @throws {Error} Throws an error if the truncation operation fails. */ async truncate() { try { await this.db(TIME_SAVINGS_TABLE).truncate(); return this.ok(true, "Table truncated successfully"); } catch (error) { return this.fail(error, "Error truncating table"); } } /** * Retrieves the template name associated with a given template task ID. * * This asynchronous method queries the TIME_SAVINGS_TABLE to find the template name * corresponding to the specified templateTaskId. It returns the template name if found, * or undefined if no matching record exists. The method handles any errors that may occur during the query. * * @param {string} templateTaskId - The ID of the template task for which the template name is to be retrieved. * @returns {Promise<string | undefined | void>} A promise that resolves to the template name as a string if found, * or undefined if no record matches the provided ID. Returns void if an error occurs during the operation. * @throws {Error} Throws an error if the retrieval process fails. */ async getTemplateNameByTemplateTaskId(templateTaskId) { try { const result = await this.db( TIME_SAVINGS_TABLE ).select("template_name").where("template_task_id", templateTaskId).limit(1).first(); return this.ok( result ? result.template_name : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error, undefined, "getTemplateNameByTemplateTaskId"); } } /** * Retrieves statistics on time saved by each team for a specific template task ID. * * This asynchronous method queries the TIME_SAVINGS_TABLE to calculate the total time saved * by each team associated with the provided templateTaskId. The results are grouped by team, * and the method returns an array of time saved statistics as DTOs, or undefined if no data is found. * * @param {string} templateTaskId - The ID of the template task for which time saved statistics are to be retrieved. * @returns {Promise<TimeSavedStatisticsByTeamName[] | undefined | void>} A promise that resolves to an array of time saved statistics * as DTOs if records are found, or undefined if no records match the provided ID. Returns void if an error occurs during the operation. * @throws {Error} Throws an error if the retrieval process fails. */ async getStatsByTemplateTaskId(templateTaskId) { try { const result = await this.db( TIME_SAVINGS_TABLE ).sum({ time_saved: "time_saved" }).select("team").where("template_task_id", templateTaskId).groupBy("team"); return this.ok( result && result.length ? result.map( (e) => mappers.TimeSavedStatisticsMap.toDTO(e) ) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error, undefined, "getStatsByTemplateTaskId"); } } /** * Retrieves statistics on time saved by each template for a specific team. * * This asynchronous method queries the TIME_SAVINGS_TABLE to calculate the total time saved * by each template associated with the provided team name. The results are grouped by template name * and team, returning an array of time saved statistics as DTOs, or undefined if no data is found. * * @param {string} team - The name of the team for which time saved statistics are to be retrieved. * @returns {Promise<TimeSavedStatisticsByTemplateName[] | undefined | void>} A promise that resolves to an array of time saved statistics * as DTOs if records are found, or undefined if no records match the provided team name. Returns void if an error occurs during the operation. * @throws {Error} Throws an error if the retrieval process fails. */ async getStatsByTeam(team) { try { const result = await this.db( TIME_SAVINGS_TABLE ).sum({ time_saved: "time_saved" }).select("template_name").where("team", team).groupBy( "template_name", "team" ); return this.ok( result && result.length ? result.map( (e) => mappers.TimeSavedStatisticsMap.toDTO( e ) ) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error, undefined, "getStatsByTeam"); } } /** * Retrieves statistics on time saved by each team for a specific template. * * This asynchronous method queries the TIME_SAVINGS_TABLE to calculate the total time saved * by each team associated with the provided template name. The results are grouped by team, * returning an array of time saved statistics as DTOs, or undefined if no data is found. * * @param {string} template - The name of the template for which time saved statistics are to be retrieved. * @returns {Promise<TimeSavedStatisticsByTemplateName[] | undefined | void>} A promise that resolves to an array of time saved statistics * as DTOs if records are found, or undefined if no records match the provided template name. Returns void if an error occurs during the operation. * @throws {Error} Throws an error if the retrieval process fails. */ async getStatsByTemplate(template) { try { const result = await this.db( TIME_SAVINGS_TABLE ).sum({ time_saved: "time_saved" }).select("team").where("template_name", template).groupBy( "template_name", "team" ); return this.ok( result && result.length ? result.map( (e) => mappers.TimeSavedStatisticsMap.toDTO(e) ) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error, undefined, "getStatsByTemplate"); } } /** * Retrieves statistics on time saved by each team for all templates. * * This asynchronous method queries the TIME_SAVINGS_TABLE to calculate the total time saved * by each team across all templates. The results are grouped by team and template name, * returning an array of time saved statistics as DTOs, or undefined if no data is found. * * @returns {Promise<TimeSavedStatistics[] | undefined | void>} A promise that resolves to an array of time saved statistics * as DTOs if records are found, or undefined if no records exist. Returns void if an error occurs during the operation. * @throws {Error} Throws an error if the retrieval process fails. */ async getAllStats() { try { const result = await this.db( TIME_SAVINGS_TABLE ).sum({ time_saved: "time_saved" }).select("team", "template_name").groupBy("team", "template_name"); return this.ok( result && result.length ? result.map( (e) => mappers.TimeSavedStatisticsMap.toDTO(e) ) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error, undefined, "getAllStats"); } } /** * Calculates the percentage of time savings for each team relative to the total time saved by all teams. * * This asynchronous method performs a query to compute the total time saved by each team and * then calculates the percentage of each team's savings compared to the overall savings. * The results are returned as an array of group savings division statistics as DTOs, or undefined if no data is found. * * @returns {Promise<GroupSavingsDivision[] | undefined | void>} A promise that resolves to an array of group savings division statistics * as DTOs if records are found, or undefined if no records exist. Returns void if an error occurs during the operation. * @throws {Error} Throws an error if the calculation process fails. */ async getGroupSavingsDivision() { try { let roundingFunction; let windowedSumFunction; const { client } = this.db.client.config; if (client === "pg") { roundingFunction = "ROUND((SUM(main.time_saved) / sub.total_team_time_saved)::numeric * 100, 2)"; windowedSumFunction = "SUM(time_saved) OVER ()"; } else if (["mysql", "mysql2"].includes(client)) { roundingFunction = "ROUND((SUM(main.time_saved) / sub.total_team_time_saved) * 100, 2)"; windowedSumFunction = "SUM(time_saved) OVER ()"; } else if (client === "mssql") { roundingFunction = "ROUND((SUM(main.time_saved) / sub.total_team_time_saved) * 100, 2)"; windowedSumFunction = "SUM(time_saved) OVER ()"; } else if (client === "better-sqlite3") { roundingFunction = "ROUND((SUM(main.time_saved) / sub.total_team_time_saved) * 100, 2)"; windowedSumFunction = `(SELECT SUM(time_saved) FROM ${TIME_SAVINGS_TABLE})`; } else { throw new Error(`Unsupported database client: ${client}`); } const subquery = this.db("ts_template_time_savings as sub").select( "team", this.db.raw(`${windowedSumFunction} AS total_team_time_saved`) ).groupBy("team", "time_saved"); const result = await this.db( "ts_template_time_savings as main" ).select("main.team", this.db.raw(`${roundingFunction} as percentage`)).innerJoin(subquery.as("sub"), "main.team", "sub.team").groupBy("main.team", "sub.total_team_time_saved"); return this.ok( result && result.length ? result.map((e) => mappers.GroupSavingsDivisionMap.toDTO(e)) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error, undefined, "getGroupSavingsDivision"); } } /** * Retrieves daily time summaries of total time saved by each team. * * This asynchronous method queries the `ts_template_time_savings` table to calculate the total time saved * by each team, grouped by year and team. The results are ordered by date in descending order, returning * an array of time summary statistics as DTOs, or undefined if no data is found. * * @returns {Promise<TimeSummary[] | undefined | void>} A promise that resolves to an array of daily time summaries * as DTOs if records are found, or undefined if no records exist. Returns void if an error occurs during the operation. * @throws {Error} Throws an error if the retrieval process fails. */ async getDailyTimeSummariesByTeam() { try { const formattedDate = this.formatDate(this.db, "created_at", "date"); const formattedGroupBy = this.formatGroupBy(this.db, "created_at"); const result = await this.db( "ts_template_time_savings" ).sum({ total_time_saved: "time_saved" }).select(formattedDate, "team").groupByRaw(`${formattedGroupBy}, team`).orderBy("date"); return this.ok( result && result.length ? result.map((e) => mappers.TimeSummaryMap.timeSummaryByTeamNameToDTO(e)) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error, undefined, "getDailyTimeSummariesByTeam"); } } /** * Retrieves daily time summaries of total time saved by each template. * * This asynchronous method queries the `ts_template_time_savings` table to calculate the total time saved * by each template, grouped by year and template name. The results are ordered by date in descending order, * returning an array of time summary statistics as DTOs, or undefined if no data is found. * * @returns {Promise<TimeSummary[] | undefined | void>} A promise that resolves to an array of daily time summaries * as DTOs if records are found, or undefined if no records exist. Returns void if an error occurs during the operation. * @throws {Error} Throws an error if the retrieval process fails. */ async getDailyTimeSummariesByTemplate() { try { const formattedDate = this.formatDate(this.db, "created_at", "date"); const formattedGroupBy = this.formatGroupBy(this.db, "created_at"); const result = await this.db( "ts_template_time_savings" ).sum("time_saved as total_time_saved").select(formattedDate, "template_name").groupByRaw(`${formattedGroupBy}, template_name`).orderBy("date"); return this.ok( result && result.length ? result.map((e) => mappers.TimeSummaryMap.timeSummaryByTemplateNameToDTO(e)) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error, undefined, "getDailyTimeSummariesByTemplate"); } } /** * Retrieves time summaries of total time saved by each team, grouped by date. * * This asynchronous method performs a subquery to calculate the total time saved by each team for each date, * and then aggregates these results to provide a summary of time saved by team on a daily basis. * The results are ordered by date, returning an array of time summary statistics as DTOs, or undefined if no data is found. * * @returns {Promise<TimeSummary[] | undefined | void>} A promise that resolves to an array of time summaries * as DTOs if records are found, or undefined if no records exist. Returns void if an error occurs during the operation. * @throws {Error} Throws an error if the retrieval process fails. */ async getTimeSavedSummaryByTeam() { try { const formattedDate = this.formatDate(this.db, "created_at", "date"); const subquery = this.db("ts_template_time_savings as sub").select( "team", formattedDate, this.db.raw("SUM(time_saved) as total_time_saved") ).groupBy("template_name", "date", "team"); const result = await this.db( subquery.as("temp") ).select( "temp.date", "team", this.db.raw("SUM(total_time_saved) as total_time_saved") ).groupBy("team", "date").orderBy("date"); this.logger.info(`APPA :: ${subquery} | ${JSON.stringify(result)}`); return this.ok( result && result.length ? result.map((e) => mappers.TimeSummaryMap.timeSummaryByTeamNameToDTO(e)) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error, undefined, "getTimeSavedSummaryByTeam"); } } /** * Retrieves time summaries of total time saved by each template, grouped by date. * * This asynchronous method performs a subquery to calculate the total time saved for each template on each date, * aggregating the results to provide a summary of time saved by template on a daily basis. * The results are ordered by date, returning an array of time summary statistics as DTOs, or undefined if no data is found. * * @returns {Promise<TimeSummary[] | undefined | void>} A promise that resolves to an array of time summaries * as DTOs if records are found, or undefined if no records exist. Returns void if an error occurs during the operation. * @throws {Error} Throws an error if the retrieval process fails. */ async getTimeSavedSummaryByTemplate() { try { const formattedDate = this.formatDate(this.db, "created_at", "date"); const subquery = this.db( "ts_template_time_savings as sub" ).select( "template_name", formattedDate, this.db.raw("SUM(time_saved) as total_time_saved") ).groupBy("template_name", "date"); const result = await this.db.select( "date", "template_name", this.db.raw("SUM(total_time_saved) as total_time_saved") ).from(subquery.as("temp")).groupBy("template_name", "date").orderBy("date"); return this.ok( result && result.length ? result.map((e) => mappers.TimeSummaryMap.timeSummaryByTemplateNameToDTO(e)) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error, undefined, "getTimeSavedSummaryByTemplate"); } } /** * Retrieves distinct values from a specified column in the TIME_SAVINGS_TABLE. * * This asynchronous method queries the database to obtain unique entries from the specified column. * The results are then mapped to a DTO format for easier consumption. The method returns an object containing * the distinct values or undefined if no data is found. * * @param {string} column - The name of the column from which to retrieve distinct values. * @returns {Promise<{ [x: string]: (string | number)[] } | undefined | void>} A promise that resolves to an object containing * distinct values from the specified column, or undefined if no records exist. Returns void if an error occurs during the operation. * @throws {Error} Throws an error if the retrieval process fails. */ async getDistinctColumn(column) { try { const result = await this.db( TIME_SAVINGS_TABLE ).distinct(column); this.logger.debug(`getDistinctColumn - ${JSON.stringify(result)}`); return this.ok( mappers.TemplateTimeSavingsCollectionMap.distinctToDTO(result), "Data selected successfully" ); } catch (error) { return this.fail(error, undefined, "getDistinctColumn"); } } /** * Retrieves the count of distinct template task IDs in the TIME_SAVINGS_TABLE. * * This asynchronous method queries the database to count the unique template task IDs present in the specified table. * It returns the count as a number, defaulting to zero if no templates are found. The method handles any errors that may occur during the query. * * @returns {Promise<number | void>} A promise that resolves to the count of distinct template task IDs, * or zero if no records exist. Returns void if an error occurs during the operation. * @throws {Error} Throws an error if the retrieval process fails. */ async getTemplateCount() { try { const result = await this.db(TIME_SAVINGS_TABLE).countDistinct("template_task_id as count").first(); return this.ok( parseInt(result?.count, 10) || 0, "Data selected successfully" ); } catch (error) { return this.fail(error, undefined, "getTemplateCount"); } } /** * Calculates the total sum of values in a specified column from the TIME_SAVINGS_TABLE. * * This asynchronous method queries the database to compute the sum of the values in the given column. * It returns the total sum as a number, defaulting to zero if no values are found. The method also handles * any errors that may occur during the query execution. * * @param {string} column - The name of the column for which the sum of values is to be calculated. * @returns {Promise<number | void>} A promise that resolves to the total sum of the specified column, * or zero if no records exist. Returns void if an error occurs during the operation. * @throws {Error} Throws an error if the retrieval process fails. */ async getTimeSavedSum() { try { const result = await this.db(TIME_SAVINGS_TABLE).sum({ sum: "time_saved" }).first(); return this.ok(result?.sum || 0, "Data selected successfully"); } catch (error) { return this.fail(error, undefined, "getTimeSavedSum"); } } } exports.TimeSaverDatabase = TimeSaverDatabase; //# sourceMappingURL=TimeSaverDatabase.cjs.js.map