UNPKG

@tduniec/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

1,315 lines (1,303 loc) 62 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var backendPluginApi = require('@backstage/backend-plugin-api'); var backendCommon = require('@backstage/backend-common'); var express = require('express'); var Router = require('express-promise-router'); var luxon = require('luxon'); var database = require('@backstage/backend-defaults/database'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var express__default = /*#__PURE__*/_interopDefaultCompat(express); var Router__default = /*#__PURE__*/_interopDefaultCompat(Router); class ScaffolderClient { constructor(logger, config, auth) { this.logger = logger; this.config = config; this.auth = auth; } /** * Fetch a page of templates with pagination support. */ async fetchTemplatesFromScaffolder(opts = {}) { const { page = 0, pageSize = 50 } = opts; let backendUrl = this.config.getOptionalString("ts.backendUrl") ?? "http://127.0.0.1:7007"; backendUrl = backendUrl.replace( /(http:\/\/)localhost(:\d+)/g, "$1127.0.0.1$2" ); const templatePath = "/api/scaffolder/v2/tasks"; const offset = page * pageSize; const callUrl = `${backendUrl}${templatePath}?limit=${pageSize}&offset=${offset}`; const token = await this.generateBackendToken(); try { const response = await fetch(callUrl, { method: "GET", headers: { Authorization: `Bearer ${token}` } }); const data = await response.json(); this.logger.debug( `Scaffolder API response (page=${page}, size=${pageSize}): ${JSON.stringify( data )}` ); if (Object.hasOwn(data, "error")) { this.logger.error("Error retrieving scaffolder tasks", data.error); return []; } if (!Array.isArray(data.tasks)) { this.logger.error("Unexpected response: tasks array missing"); return []; } return data.tasks; } catch (error) { this.logger.error(`Failed to fetch from ${callUrl}`, error); return []; } } /** * Stream all templates, page by page, yielding each task as it arrives. */ async *streamTemplatesFromScaffolder(pageSize = 50) { let page = 0; while (true) { const batch = await this.fetchTemplatesFromScaffolder({ page, pageSize }); if (!batch.length) break; for (const task of batch) { yield task; } page++; } } async generateBackendToken() { const { token } = await this.auth.getPluginRequestToken({ onBehalfOf: await this.auth.getOwnServiceCredentials(), targetPluginId: "scaffolder" }); return token; } } class TimeSaverHandler { constructor(logger, config, auth, db) { this.logger = logger; this.config = config; this.auth = auth; this.db = db; } async fetchTemplates() { const pageSize = this.config.getOptionalNumber("ts.scheduler.parallelProcessing") ?? 100; this.logger.debug(`SET parallelProcessing of tasks to: ${pageSize}`); const client = new ScaffolderClient(this.logger, this.config, this.auth); this.logger.info("START \u2013 Collecting Time Savings data from templates"); let excludedSet = /* @__PURE__ */ new Set(); try { const excluded = await this.db.getTasksToExclude(); if (Array.isArray(excluded)) excludedSet = new Set(excluded); } catch (e) { this.logger.error("Failed to load exclusion list", e); return "FAIL"; } await this.db.truncate(); for (let page = 0; ; page++) { this.logger.debug(`Fetching page ${page} (size=${pageSize})`); const tasks = await client.fetchTemplatesFromScaffolder({ page, pageSize }); if (tasks.length === 0) break; const rows = []; for (const tpl of tasks) { if (tpl.status !== "completed" || excludedSet.has(tpl.id)) { continue; } const subs = tpl.spec.templateInfo.entity.metadata.substitute?.engineering; if (!subs) { continue; } const createdAt = luxon.DateTime.fromISO(tpl.createdAt, { setZone: true }); if (!createdAt.isValid) { this.logger.error( `Invalid createdAt for template ${tpl.id}: ${tpl.createdAt}` ); continue; } for (const [team, timeSaved] of Object.entries(subs)) { rows.push({ team, role: "", timeSaved, createdAt, createdBy: tpl.createdBy, templateName: tpl.spec.templateInfo.entityRef, templateTaskStatus: tpl.status, templateTaskId: tpl.id }); } } if (rows.length) { this.logger.debug(`Inserting ${rows.length} rows`); await this.db.bulkInsertTimeSavings(rows); } } this.logger.info("STOP \u2013 Collecting Time Savings data from templates"); return "SUCCESS"; } } const DEFAULT_SAMPLE_CLASSIFICATION = { engineering: { devops: 8, development_team: 8, security: 3 } }; const DEFAULT_SAMPLE_TEMPLATES_TASKS = [ "template:default/create-github-project", "template:default/create-nodejs-service", "template:default/create-golang-service" ]; class TimeSaverApi { constructor(logger, config, auth, timeSaverDb, scaffolderDb) { this.logger = logger; this.config = config; this.auth = auth; this.timeSaverDb = timeSaverDb; this.scaffolderDb = scaffolderDb; } async getStatsByTemplateTaskId(templateTaskId, query) { const templateName = await this.timeSaverDb.getTemplateNameByTsId( templateTaskId, query ); const queryResult = await this.timeSaverDb.getStatsByTemplateTaskId( templateTaskId, query ); const outputBody = { templateTaskId, templateName, stats: queryResult }; this.logger.debug(JSON.stringify(outputBody)); return outputBody; } async getStatsByTeam(team, query) { const queryResult = await this.timeSaverDb.getStatsByTeam(team, query); const outputBody = { team, stats: queryResult || [] }; this.logger.debug(JSON.stringify(outputBody)); return outputBody; } async getStatsByTemplate(template, query) { const queryResult = await this.timeSaverDb.getStatsByTemplate( template, query ); const outputBody = { template_name: template, stats: queryResult || [] }; this.logger.debug(JSON.stringify(outputBody)); return outputBody; } async getAllStats(query) { const queryResult = await this.timeSaverDb.getAllStats(query); const outputBody = { stats: queryResult || [] }; this.logger.debug(JSON.stringify(outputBody)); return outputBody; } async getGroupDivisionStats(query) { const queryResult = await this.timeSaverDb.getGroupSavingsDivision(query); const outputBody = { stats: queryResult || [] }; this.logger.debug(JSON.stringify(outputBody)); return outputBody; } async getDailyTimeSummariesTeamWise(query) { const queryResult = await this.timeSaverDb.getDailyTimeSummariesTeamWise( query ); const outputBody = { stats: queryResult || [] }; this.logger.debug(JSON.stringify(outputBody)); return outputBody; } async getDailyTimeSummariesTemplateWise(query) { const queryResult = await this.timeSaverDb.getDailyTimeSummariesTemplateWise(query); const outputBody = { stats: queryResult || [] }; this.logger.debug(JSON.stringify(outputBody)); return outputBody; } async getTimeSummarySavedTeamWise(query) { const queryResult = await this.timeSaverDb.getTimeSummarySavedTeamWise( query ); const outputBody = { stats: queryResult || [] }; this.logger.debug(JSON.stringify(outputBody)); return outputBody; } async getTimeSummarySavedTemplateWise(query) { const queryResult = await this.timeSaverDb.getTimeSummarySavedTemplateWise( query ); const outputBody = { stats: queryResult || [] }; this.logger.debug(JSON.stringify(outputBody)); return outputBody; } async getSampleMigrationClassificationConfig(customClassificationRequest, options) { if (typeof customClassificationRequest === "object" && !Object.keys(customClassificationRequest).length) { const errorMessage = `getSampleMigrationClassificationConfig : customClassificationRequest cannot be an empty object`; this.logger.error( `getSampleMigrationClassificationConfig : customClassificationRequest cannot be an empty object` ); return { status: "FAIL", errorMessage }; } const sampleClassification = customClassificationRequest || DEFAULT_SAMPLE_CLASSIFICATION; const templatesList = options?.useScaffolderTasksEntries ? (await this.getAllTemplateTasks()).templateTasks : DEFAULT_SAMPLE_TEMPLATES_TASKS; this.logger.debug( `Generating sample classification configuration with ${options?.useScaffolderTasksEntries ? "scaffolder DB" : "user-defined"} templates tasks list and ${customClassificationRequest ? "user-defined" : "default"} classification` ); return { status: "OK", data: templatesList.map((t) => ({ entityRef: t, ...sampleClassification })) }; } async getAllGroups(query) { let groups; let outputBody = { groups: [], errorMessage: "" }; const queryResult = await this.timeSaverDb.getDistinctColumn("team", query); if (queryResult && queryResult.team.length > 0) { groups = queryResult.team.map((e) => e.toString()); outputBody = { ...outputBody, groups }; this.logger.debug(JSON.stringify(outputBody)); } else { const errorMessage = "getAllGroups - DB returned 0 rows"; outputBody = { ...outputBody, errorMessage }; this.logger.warn(errorMessage); } return outputBody; } async getAllTemplateNames(query) { let templates; let outputBody = { templates: [], errorMessage: "" }; const queryResult = await this.timeSaverDb.getDistinctColumn( "template_name", query ); if (queryResult && queryResult.template_name.length > 0) { templates = queryResult.template_name.map((e) => e.toString()); outputBody = { ...outputBody, templates }; this.logger.debug(JSON.stringify(outputBody)); } else { const errorMessage = "getAllGroups - DB returned 0 rows"; outputBody = { ...outputBody, errorMessage }; this.logger.warn(errorMessage); } return outputBody; } async getAllTemplateTasks() { let templateTasks; let outputBody = { templateTasks: [], errorMessage: "" }; const queryResult = await this.timeSaverDb.getDistinctColumn( "template_task_id", {} ); if (queryResult && queryResult.template_task_id.length > 0) { templateTasks = queryResult.template_task_id.map((e) => e.toString()); outputBody = { ...outputBody, templateTasks }; this.logger.debug(JSON.stringify(outputBody)); } else { const errorMessage = "getAllGroups - DB returned 0 rows"; outputBody = { ...outputBody, errorMessage }; this.logger.warn(errorMessage); } return outputBody; } async getTemplateCount(query) { let outputBody = { templateCount: 0, errorMessage: "" }; const queryResult = await this.timeSaverDb.getTemplateCount(query); if (typeof queryResult === "number") { outputBody = { templateCount: queryResult }; this.logger.debug(`getTemplateCount: ${JSON.stringify(outputBody)}`); } else { const errorMessage = "getTemplateCount did not return any results"; outputBody = { ...outputBody, errorMessage }; this.logger.warn(errorMessage); } return outputBody; } async getTimeSavedSum(divider, query) { let outputBody = { timeSaved: 0, errorMessage: "" }; const dividerInt = divider ?? 1; const queryResult = await this.timeSaverDb.getTimeSavedSum(query); if (typeof queryResult === "number") { outputBody = { timeSaved: queryResult ? queryResult / dividerInt : queryResult }; this.logger.debug(JSON.stringify(outputBody)); } else { const errorMessage = "getTimeSavedSum - DB returned 0 rows"; outputBody = { ...outputBody, errorMessage }; this.logger.warn(errorMessage); } return outputBody; } async updateTemplatesWithSubstituteData(requestData) { let templateClassification; let migrationStatisticsReport = { updatedTemplates: { total: 0, list: [] }, missingTemplates: { total: 0, list: [] } }; if (requestData) { try { if (typeof requestData !== "object") { templateClassification = JSON.parse(requestData); } else { templateClassification = requestData; } if (!templateClassification || !Object.keys(templateClassification).length) { throw new Error( `Invalid classification ${JSON.stringify( requestData )}. Either it was empty or could not parse JSON string. Aborting...` ); } this.logger.debug( `Found classification in API POST body: ${JSON.stringify( templateClassification )}` ); } catch (error) { const msg = `Migration: Could not parse JSON object from POST call body "${JSON.stringify( requestData )}", aborting...`; this.logger.error(msg, error ? error : void 0); return { status: "FAIL", message: `${msg} - ${error}` }; } } else { const tsConfigObj = this.config.getOptionalString("ts.backward.config") || void 0; if (!tsConfigObj) { const errorMessage = "Migration: Could not find backward migration configuration in app-config.x.yaml, aborting..."; this.logger.error(errorMessage); return { status: "FAIL", message: errorMessage }; } try { templateClassification = JSON.parse(String(tsConfigObj)); this.logger.debug( `Found classification in app-config.x.yaml: ${JSON.stringify( templateClassification )}` ); } catch (error) { const msg = "Migration: Could not parse backward migration configuration as JSON object from app-config.x.yaml, aborting..."; this.logger.error(msg, error ? error : void 0); return { status: "FAIL", message: `${msg} - ${error}` }; } } try { this.logger.info(`Starting backward migration`); const taskTemplateList = await new ScaffolderClient( this.logger, this.config, this.auth ).fetchTemplatesFromScaffolder(); for (let i = 0; i < taskTemplateList.length; i++) { const scaffolderTaskRecord = taskTemplateList[i]; this.logger.debug( `Migrating template ${JSON.stringify(scaffolderTaskRecord)}` ); const { entityRef: templateEntityRef } = scaffolderTaskRecord.spec.templateInfo; this.logger.debug( `Found template with entityRef: ${templateEntityRef}` ); const classificationEntry = templateClassification.find( (con) => con.entityRef === templateEntityRef ); if (classificationEntry) { const newClassificationEntry = Object.assign( {}, classificationEntry ); delete newClassificationEntry.entityRef; const newTemplateTaskRecordSpecs = { ...scaffolderTaskRecord.spec, templateInfo: { ...scaffolderTaskRecord.spec.templateInfo, entity: { ...scaffolderTaskRecord.spec.templateInfo.entity, metadata: { ...scaffolderTaskRecord.spec.templateInfo.entity.metadata, substitute: newClassificationEntry } } } }; const patchQueryResult = await this.scaffolderDb.updateTemplateTaskById( scaffolderTaskRecord.id, JSON.stringify(newTemplateTaskRecordSpecs) ); if (patchQueryResult) { migrationStatisticsReport = { ...migrationStatisticsReport, updatedTemplates: { total: ++migrationStatisticsReport.updatedTemplates.total, list: [ ...migrationStatisticsReport.updatedTemplates.list, scaffolderTaskRecord.id ] } }; this.logger.debug( `scaffolderTaskRecord with id ${scaffolderTaskRecord.id} was patched` ); } } else { migrationStatisticsReport = { ...migrationStatisticsReport, missingTemplates: { total: ++migrationStatisticsReport.missingTemplates.total, list: [ ...migrationStatisticsReport.missingTemplates.list, scaffolderTaskRecord.id ] } }; this.logger.debug( `scaffolderTaskRecord with id ${scaffolderTaskRecord.id} was not found in scaffolder DB` ); } } } catch (error) { this.logger.error( `Could not continue with backward migration, aborting...`, error ? error : void 0 ); return { status: "error", error: error ? error : void 0 }; } return { status: "SUCCESS", migrationStatisticsReport }; } } class ScaffolderDatabase { constructor(knex, logger) { this.knex = knex; this.logger = logger; } static async create(config, logger) { const db = database.DatabaseManager.fromConfig(config).forPlugin("scaffolder"); const knex = await db.getClient(); return new ScaffolderDatabase(knex, logger); } async collectSpecByTemplateId(templateTaskId) { try { const result = await this.knex("tasks").select("spec").where("id", templateTaskId); this.logger.debug( `collectSpecByTemplateId : Data selected successfully ${JSON.stringify( result )}` ); return result; } catch (error) { this.logger.error( "Error selecting data:", error ? error : void 0 ); throw error; } } async updateTemplateTaskById(templateTaskId, templateTaskSpecs) { try { const result = await this.knex("tasks").where({ id: templateTaskId }).update({ spec: templateTaskSpecs }); this.logger.debug( `updateTemplateTaskById : Data selected successfully ${JSON.stringify( result )}` ); return result; } catch (error) { this.logger.error( "Error selecting data:", error ? error : void 0 ); throw error; } } } function roundNumericValues(obj) { const roundValue = (value) => { const rounded = Math.round(value * 100) / 100; if (Number.isInteger(rounded)) { return rounded; } return parseFloat(rounded.toFixed(2)); }; const roundObject = (input) => { if (typeof input === "object" && input !== null) { Object.values(input).map((value) => { switch (typeof value) { case "number": return roundValue(value); case "object": return roundObject(value); default: return value; } }); } return input; }; return roundObject(obj); } function dateTimeFromIsoDate(isoDate = "") { return luxon.DateTime.fromJSDate(new Date(isoDate)); } function isoDateFromDateTime(dateTime) { return dateTime.toISO(); } const DEFAULT_DB_CREATED_AT_VALUE = ""; class TemplateTimeSavingsMap { static toPersistence(templateTimeSavings) { return { team: templateTimeSavings.team, role: templateTimeSavings.role, created_at: isoDateFromDateTime(templateTimeSavings.createdAt) || DEFAULT_DB_CREATED_AT_VALUE, created_by: templateTimeSavings.createdBy, time_saved: templateTimeSavings.timeSaved, template_name: templateTimeSavings.templateName, template_task_id: templateTimeSavings.templateTaskId, template_task_status: templateTimeSavings.templateTaskStatus }; } static toDTO(templateTimeSavingsDbRow) { return { id: templateTimeSavingsDbRow.id, team: templateTimeSavingsDbRow.team, role: templateTimeSavingsDbRow.role, createdAt: dateTimeFromIsoDate(templateTimeSavingsDbRow.created_at), createdBy: templateTimeSavingsDbRow.created_by, timeSaved: roundNumericValues(templateTimeSavingsDbRow.time_saved), templateName: templateTimeSavingsDbRow.template_name, templateTaskId: templateTimeSavingsDbRow.template_task_id, templateTaskStatus: templateTimeSavingsDbRow.template_task_status }; } } class TemplateTimeSavingsCollectionMap { static toDTO(templateTimeSavingsDbRows) { return templateTimeSavingsDbRows.map((e) => TemplateTimeSavingsMap.toDTO(e)); } static distinctToDTO(templateTimeSavingsDbRows) { if (!(templateTimeSavingsDbRows && templateTimeSavingsDbRows.length)) { return void 0; } const key = Object.keys(templateTimeSavingsDbRows[0])[0]; const values = templateTimeSavingsDbRows.map((e) => Object.values(e)[0]); return { [key]: [...values] }; } } class TimeSavedStatisticsMap { static toDTO(timeSavedStatisticsDbRow) { return { team: timeSavedStatisticsDbRow?.team, templateName: timeSavedStatisticsDbRow?.template_name, timeSaved: parseInt(timeSavedStatisticsDbRow?.time_saved || "0", 10) }; } } class GroupSavingsDivisionMap { static toDTO(groupSavingsDivisionDbRow) { return { team: groupSavingsDivisionDbRow?.team, percentage: roundNumericValues(groupSavingsDivisionDbRow.percentage) }; } } class TimeSummaryMap { static toDTO(timeSummaryDbRow) { return { team: timeSummaryDbRow?.team, templateName: timeSummaryDbRow?.template_name, date: dateTimeFromIsoDate(timeSummaryDbRow.date), totalTimeSaved: roundNumericValues(timeSummaryDbRow.total_time_saved) || 0 }; } } const TIME_SAVINGS_TABLE = "ts_template_time_savings"; const EXCLUDED_TASKS_TABLE = "ts_excluded_tasks_everywhere"; const migrationsDir = backendPluginApi.resolvePackagePath( "@tduniec/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:") { this.logger.error(errorMessage, error ? error : void 0); throw error; } createBuilderWhereDates(builder, query) { const { start, end } = query || {}; if (start && end) { builder.whereBetween("created_at", [ `${start}T00:00:00`, `${end}T23:59:59` ]); } else if (start) { builder.where("created_at", ">=", `${start}T00:00:00`); } else if (end) { builder.where("created_at", "<=", `${end}T23:59:59`); } return builder; } /** * 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") { return knex.raw(`DATE_FORMAT(${column}, '%Y-%m-%d') as ${alias}`); } else if (client === "mssql") { return knex.raw(`FORMAT(${column}, 'yyyy-MM-dd') as ${alias}`); } return knex.raw(`TO_CHAR(${column}, 'YYYY-MM-DD') as ${alias}`); } /** * 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(TemplateTimeSavingsMap.toPersistence(templateTimeSavings)).returning("*"); this.logger.debug(`insertedRows: ${JSON.stringify(insertedRows)}`); return this.ok( insertedRows && insertedRows.length ? 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(TemplateTimeSavingsMap.toPersistence(data)).returning("*"); this.logger.debug(`updatedRows: ${JSON.stringify(result)}`); return this.ok( result.length > 0 ? 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 ? 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 getTemplateNameByTsId(templateTaskId, query) { try { const dbPromise = this.createBuilderWhereDates( this.db(TIME_SAVINGS_TABLE), query ); const result = await dbPromise.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); } } /** * 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<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 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, query) { try { const dbPromise = this.createBuilderWhereDates( this.db(TIME_SAVINGS_TABLE), query ); const result = await dbPromise.sum({ time_saved: "time_saved" }).select("team").where("template_task_id", templateTaskId).groupBy("team"); return this.ok( result && result.length ? result.map( (e) => TimeSavedStatisticsMap.toDTO(e) ) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error); } } /** * 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<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 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, query) { try { const dbPromise = this.createBuilderWhereDates( this.db(TIME_SAVINGS_TABLE), query ); const result = await dbPromise.sum({ time_saved: "time_saved" }).select("template_name", "team").where("team", team).groupBy("template_name").groupBy("team"); return this.ok( result && result.length ? result.map( (e) => TimeSavedStatisticsMap.toDTO(e) ) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error); } } /** * 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<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 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, query) { try { const dbPromise = this.createBuilderWhereDates( this.db(TIME_SAVINGS_TABLE), query ); const result = await dbPromise.sum({ time_saved: "time_saved" }).select("team").where("template_name", template).groupBy("team").groupBy("template_name"); return this.ok( result && result.length ? result.map( (e) => TimeSavedStatisticsMap.toDTO(e) ) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error); } } /** * 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(query) { try { const dbPromise = this.createBuilderWhereDates( this.db(TIME_SAVINGS_TABLE), query ); const result = await dbPromise.sum({ time_saved: "time_saved" }).select("team", "template_name").groupBy("team", "template_name"); return this.ok( result && result.length ? result.map( (e) => TimeSavedStatisticsMap.toDTO(e) ) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error); } } /** * 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(query) { try { const subquery = this.createBuilderWhereDates( this.db(`${TIME_SAVINGS_TABLE} as sub`), query ).select("team").sum("time_saved as total_team_time_saved").groupBy("team"); const dbPromise = this.createBuilderWhereDates( this.db(TIME_SAVINGS_TABLE), query ); const total = await dbPromise.as("total").sum("time_saved as sum").first().then((data) => { return data?.sum ?? 0; }); const result = await this.createBuilderWhereDates( this.db(`${TIME_SAVINGS_TABLE} as main`), query ).select("main.team").innerJoin(subquery.as("sub"), "main.team", "sub.team").select( "main.team", // TODO: ROUND(...) function fails, temporary replaced with linear calculation this.db.raw( `(sub.total_team_time_saved / ${total}) * 100 as percentage` ) ).groupBy("main.team", "sub.total_team_time_saved"); return this.ok( result && result.length ? result.map( (e) => GroupSavingsDivisionMap.toDTO(e) ) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error); } } /** * 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 getDailyTimeSummariesTeamWise(query) { try { const formattedDate = this.formatDate(this.db, "created_at", "date"); const dbPromise = this.createBuilderWhereDates( this.db(TIME_SAVINGS_TABLE), query ); const result = await dbPromise.sum({ total_time_saved: "time_saved" }).select(formattedDate, "team").groupByRaw("date, team").orderByRaw("date"); return this.ok( result && result.length ? result.map((e) => TimeSummaryMap.toDTO(e)) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error); } } /** * 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 getDailyTimeSummariesTemplateWise(query) { try { const formattedDate = this.formatDate(this.db, "created_at", "date"); const dbPromise = this.createBuilderWhereDates( this.db(TIME_SAVINGS_TABLE), query ); const result = await dbPromise.sum("time_saved as total_time_saved").select(formattedDate, "template_name").groupByRaw("date, template_name").orderBy("date"); return this.ok( result && result.length ? result.map((e) => TimeSummaryMap.toDTO(e)) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error); } } /** * POSTGRES LOGIC IS A REFERENCE TODO:fix logic to work on SQLite * * 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 getTimeSummarySavedTeamWise(query) { const { client } = this.db.client.config; try { let result; const dbPromise = this.createBuilderWhereDates( this.db(TIME_SAVINGS_TABLE), query ); if (client === "pg") { result = await dbPromise.select( this.db.raw( "DISTINCT ON (team, DATE(created_at)) DATE(created_at) AS formatted_date" ), this.db.raw("DATE(created_at) AS date"), "team", this.db.raw( "SUM(time_saved) OVER (PARTITION BY team ORDER BY DATE(created_at)) AS total_time_saved" ) ).orderBy("team").orderByRaw("DATE(created_at)").orderBy("created_at"); } else { const subquery = dbPromise.select( "team", this.db.raw(`DATE(created_at) as date`), this.db.raw("SUM(time_saved) as total_time_saved") ).groupBy("template_name", "date", "team"); result = await this.db(subquery.as("temp")).select( "date", "team", this.db.raw("SUM(total_time_saved) as total_time_saved") ).groupBy("team", "date").orderBy("date"); } return this.ok( result && result.length ? result.map((e) => TimeSummaryMap.toDTO(e)) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error); } } /** * POSTGRES LOGIC IS A REFERENCE TODO:fix logic to work on SQLite * * 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 getTimeSummarySavedTemplateWise(query) { const { client } = this.db.client.config; let result; try { if (client === "pg") { const dbPromise = this.createBuilderWhereDates( this.db(TIME_SAVINGS_TABLE), query ); result = await dbPromise.select( this.db.raw( "DISTINCT ON (template_name, DATE(created_at)) DATE(created_at) AS formatted_date" ), this.db.raw("DATE(created_at) AS date"), "template_name", this.db.raw( "SUM(time_saved) OVER (PARTITION BY template_name ORDER BY DATE(created_at)) AS total_time_saved" ) ).orderBy("template_name").orderByRaw("DATE(created_at)").orderBy("created_at"); } else { const dbPromise = this.createBuilderWhereDates( this.db(`${TIME_SAVINGS_TABLE} as sub`), query ); const subquery = dbPromise.select( "template_name", this.db.raw(`DATE(created_at) as date`), this.db.raw("SUM(time_saved) as total_time_saved") ).groupBy("template_name", "date"); 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) => TimeSummaryMap.toDTO(e)) : void 0, "Data selected successfully" ); } catch (error) { return this.fail(error); } } /** * 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, query) { try { const dbPromise = this.createBuilderWhereDates( this.db(TIME_SAVINGS_TABLE), query ); const result = await dbPromise.distinct(column); return this.ok( TemplateTimeSavingsCollectionMap.distinctToDTO(result), "Data selected successfully" ); } catch (error) { return this.fail(error); } } /** * 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(query) { try { const dbPromise = this.createBuilderWhereDates( this.db(TIME_SAVINGS_TABLE), query ); const result = await dbPromise.countDistinct("template_task_id as count").first(); let count = 0; if (result?.count && !isNaN(1 * result.count)) { count = 1 * result.count; } return this.ok(count, "Data selected successfully"); } catch (error) { return this.fail(error); } } /** * 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 i