From 967b5e6a9224550cd5e9f7c41526fe5772c984b4 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Mon, 15 Jul 2024 11:33:15 -0400 Subject: [PATCH 1/2] [Tech Debt] Add eslint-plugin-jsdoc package - Configure eslint rules for JSDocs - Fix lint issues with JSDocs throughout the repo --- .mocharc.yml | 3 - eslint.config.js | 41 ++++- index.js | 33 ++-- package-lock.json | 153 ++++++++++++++++-- package.json | 1 + src/actions/action.js | 21 ++- .../format_processed_analytics_data.js | 4 +- src/actions/log_analytics_data.js | 9 +- .../process_google_analytics_results.js | 7 +- src/actions/publish_analytics_data_to_disk.js | 9 +- src/actions/publish_analytics_data_to_s3.js | 11 +- src/actions/query_google_analytics.js | 6 +- .../write_analytics_data_to_database.js | 13 +- src/app_config.js | 53 +++--- src/google_analytics/credential_loader.js | 16 +- src/google_analytics/query_authorizer.js | 6 +- src/google_analytics/query_builder.js | 6 +- src/google_analytics/service.js | 14 +- src/logger.js | 16 +- .../analytics_data_processor.js | 14 +- src/process_results/result_formatter.js | 10 +- .../result_totals_calculator.js | 8 +- src/processor.js | 8 +- src/publish/disk.js | 6 +- src/publish/postgres.js | 7 +- src/publish/s3.js | 8 +- src/report_processing_context.js | 6 +- test/actions/action.test.js | 12 +- 28 files changed, 355 insertions(+), 146 deletions(-) diff --git a/.mocharc.yml b/.mocharc.yml index 197e415c..d0504279 100644 --- a/.mocharc.yml +++ b/.mocharc.yml @@ -4,11 +4,8 @@ package: './package.json' slow: '75' spec: - 'test/**/*.js' - - 'ua/test/**/*.js' timeout: '2000' ui: 'bdd' watch-files: - 'src/**/*.js' - 'test/**/*.js' - - 'ua/src/**/*.js' - - 'ua/test/**/*.js' diff --git a/eslint.config.js b/eslint.config.js index ea7f507f..1aa0edf4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,6 +1,7 @@ const { configs: eslintConfigs } = require("@eslint/js"); const eslintPluginPrettierRecommended = require("eslint-plugin-prettier/recommended"); const globals = require("globals"); +const jsdoc = require("eslint-plugin-jsdoc"); module.exports = [ { @@ -11,10 +12,42 @@ module.exports = [ }, }, }, + eslintConfigs.recommended, + eslintPluginPrettierRecommended, { - // UA code is deprecated, so don't bother with static analysis rules. - ignores: ["ua/**/*.js"], - ...eslintConfigs.recommended, + plugins: { + jsdoc, + }, + files: ["src/**/*.js", "index.js"], + rules: { + ...jsdoc.configs.recommended.rules, + "jsdoc/check-indentation": "error", + "jsdoc/check-line-alignment": "error", + "jsdoc/check-syntax": "error", + "jsdoc/convert-to-jsdoc-comments": "error", + "jsdoc/no-bad-blocks": "error", + "jsdoc/no-blank-block-descriptions": "error", + "jsdoc/no-blank-blocks": "error", + "jsdoc/require-asterisk-prefix": "error", + "jsdoc/require-jsdoc": [ + "error", + { + checkGetters: false, + checkSetters: false, + publicOnly: true, + require: { + ArrowFunctionExpression: true, + ClassDeclaration: true, + ClassExpression: true, + FunctionDeclaration: true, + FunctionExpression: true, + MethodDefinition: true, + }, + }, + ], + "jsdoc/require-throws": "error", + "jsdoc/sort-tags": "error", + "jsdoc/tag-lines": "off", + }, }, - eslintPluginPrettierRecommended, ]; diff --git a/index.js b/index.js index 3b32fa6a..b6c9b1cb 100644 --- a/index.js +++ b/index.js @@ -22,24 +22,24 @@ const WriteAnalyticsDataToDatabase = require("./src/actions/write_analytics_data * a sequential chain of actions on each report object in the array. Some of the * actions performed are optional based on the options passed to this function. * - * @param {Object} options an object with options to be used when processing + * @param {object} options an object with options to be used when processing * all reports. - * @param {String} options.format the format of the analytics data produced. + * @param {string} options.format the format of the analytics data produced. * Accepted formats are "csv" or "json" - * @param {String} options.output a string filepath where the analytics data + * @param {string} options.output a string filepath where the analytics data * will be written to disk after processing. - * @param {Boolean} options.publish if true, the analytics data will be written + * @param {boolean} options.publish if true, the analytics data will be written * to AWS S3 after processing. - * @param {Boolean} options.realtime if true, the application will use the + * @param {boolean} options.realtime if true, the application will use the * google analytics realtime data reporting API when requesting data. Otherwise * the application uses the non-realtime data reporting API. - * @param {Boolean} options.slim if true, the application will create a smaller + * @param {boolean} options.slim if true, the application will create a smaller * data object when formatting the processed data. - * @param {Boolean} options['write-to-database'] if true, the application will + * @param {boolean} options.'write-to-database' if true, the application will * write the processed analytics data to the postgres database. - * @param {String} options.only if set, runs only the report with name + * @param {string} options.only if set, runs only the report with name * matching the passed string. - * @param {String} options.frequency if set, runs only the reports with + * @param {string} options.frequency if set, runs only the reports with * frequency matching the passed string. */ async function run(options = {}) { @@ -60,11 +60,12 @@ async function run(options = {}) { * throw so that subsequent reports will run if an error occurs in a previous * report. * - * @param {AppConfig} appConfig the application config - * @param {ReportProcessingContext} context - * @param {Object} reportConfig the configuration object for the analytics + * @param {import('./src/app_config')} appConfig the application config + * @param {import('./src/report_processing_context')} context the + * report-specific context for processing. + * @param {object} reportConfig the configuration object for the analytics * report to process. - * @returns {Promise} resolves when processing completes or has an error. + * @returns {Promise} resolves when processing completes or has an error. */ async function _processReport(appConfig, context, reportConfig) { return context.run(async () => { @@ -90,9 +91,9 @@ async function _processReport(appConfig, context, reportConfig) { * in the order provided to the processor here. The classes referenced here are * an implementation of the Chain of Responsibility design pattern. * - * @param {AppConfig} appConfig an application config instance. - * @param {winston.Logger} logger an application logger instance. - * @returns {Processor} an initialized processor instance. + * @param {import('./src/app_config')} appConfig an application config instance. + * @param {import('winston').Logger} logger an application logger instance. + * @returns {import('./src/processor')} an initialized processor instance. */ function _buildProcessor(appConfig, logger) { return new Processor([ diff --git a/package-lock.json b/package-lock.json index b8d93482..bf171730 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "chai": "^4.4.0", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jsdoc": "^48.7.0", "eslint-plugin-prettier": "^5.1.3", "globals": "^14.0.0", "mocha": "^10.2.0", @@ -2423,6 +2424,20 @@ "kuler": "^2.0.0" } }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz", + "integrity": "sha512-C3Axuq1xd/9VqFZpW4YAzOx5O9q/LP46uIQy/iNDpHG3fmPa6TBtvfglMCs3RBiBxAIi0Go97r8+jvTt55XMyQ==", + "dev": true, + "dependencies": { + "comment-parser": "1.4.1", + "esquery": "^1.6.0", + "jsdoc-type-pratt-parser": "~4.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -4163,6 +4178,15 @@ "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -4697,6 +4721,15 @@ "node": ">=14" } }, + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "dev": true, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -4976,6 +5009,12 @@ "stackframe": "^1.3.4" } }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -5089,6 +5128,73 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-jsdoc": { + "version": "48.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.7.0.tgz", + "integrity": "sha512-5oiVf7Y+ZxGYQTlLq81X72n+S+hjvS/u0upAdbpPEeaIZILK3MKN8lm/6QqKioBjm/qZ0B5XpMQUtc2fUkqXAg==", + "dev": true, + "dependencies": { + "@es-joy/jsdoccomment": "~0.46.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.5", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.6.0", + "parse-imports": "^2.1.1", + "semver": "^7.6.2", + "spdx-expression-parse": "^4.0.0", + "synckit": "^0.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/synckit": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.0.tgz", + "integrity": "sha512-7RnqIMq572L8PeEzKeBINYEJDDxpcH8JEgLwUqBd3TkofhFRbkq4QLR0u+36avGAhCRbk2nnmjcW9SE531hPDg==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/eslint-plugin-prettier": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", @@ -5235,9 +5341,9 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -6695,6 +6801,15 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -7046,7 +7161,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "devOptional": true, + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -7839,6 +7954,19 @@ "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", "optional": true }, + "node_modules/parse-imports": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.1.1.tgz", + "integrity": "sha512-TDT4HqzUiTMO1wJRwg/t/hYk8Wdp3iF/ToMIlAoVQfL1Xs/sTxq1dKWSMjMbQmIarfWKymOyly40+zmPHXMqCA==", + "dev": true, + "dependencies": { + "es-module-lexer": "^1.5.3", + "slashes": "^3.0.12" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -8767,13 +8895,10 @@ "dev": true }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "devOptional": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -8898,6 +9023,12 @@ "node": ">=8" } }, + "node_modules/slashes": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", + "integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==", + "dev": true + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -9811,7 +9942,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "devOptional": true + "dev": true }, "node_modules/yaml": { "version": "2.4.1", diff --git a/package.json b/package.json index 2200a90b..cc89be05 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "chai": "^4.4.0", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jsdoc": "^48.7.0", "eslint-plugin-prettier": "^5.1.3", "globals": "^14.0.0", "mocha": "^10.2.0", diff --git a/src/actions/action.js b/src/actions/action.js index 5b4dbb3f..619459bc 100644 --- a/src/actions/action.js +++ b/src/actions/action.js @@ -6,18 +6,21 @@ class Action { * Subclasses should override this method if there are instances where the * class should not execute on the context. * - * @param {ReportProcessingContext} context the context for the action chain. - * @returns {Boolean} true if the action should execute on the context. + * @param {import('../report_processing_context')} context the context for the + * action chain. + * @returns {boolean} true if the action should execute on the context. + * Defaults to true if a context is passed. */ - handles() { - return true; + handles(context) { + return !!context; } /** * Executes the logic of the Action using the strategy pattern and writes an * error log for debugging if an error occurs in execution. * - * @param {Context} context the context for the action chain. + * @param {import('../report_processing_context')} context the context for the + * action chain. */ async execute(context) { try { @@ -32,10 +35,12 @@ class Action { * Provides an execution strategy to the execute method. Subclasses should * override this method to provide their action specific logic. * - * @param {ReportProcessingContext} context the context for the action chain. + * @param {import('../report_processing_context')} context the context for the + * action chain. + * @returns {import('../report_processing_context')} the passed context */ - async executeStrategy() { - return; + async executeStrategy(context) { + return context; } } diff --git a/src/actions/format_processed_analytics_data.js b/src/actions/format_processed_analytics_data.js index 46abe921..5d3e023d 100644 --- a/src/actions/format_processed_analytics_data.js +++ b/src/actions/format_processed_analytics_data.js @@ -9,7 +9,9 @@ class FormatProcessedAnalyticsData extends Action { * Takes the processed analytics data from the context and changes the format * to JSON or CSV based on application and report config options. Writes the * formatted data to the context for use in subsequent actions. - * @param {ReportProcessingContext} context the context for the action chain. + * + * @param {import('../report_processing_context')} context the context for the + * action chain. */ async executeStrategy(context) { context.logger.debug("Formatting analytics data"); diff --git a/src/actions/log_analytics_data.js b/src/actions/log_analytics_data.js index 0ec6bd49..b78096c4 100644 --- a/src/actions/log_analytics_data.js +++ b/src/actions/log_analytics_data.js @@ -5,8 +5,9 @@ const Action = require("./action"); */ class LogAnalyticsData extends Action { /** - * @param {ReportProcessingContext} context the context for the action chain. - * @returns {Boolean} true if the application config is set to log analytics + * @param {import('../report_processing_context')} context the context for the + * action chain. + * @returns {boolean} true if the application config is set to log analytics * data */ handles(context) { @@ -16,7 +17,9 @@ class LogAnalyticsData extends Action { /** * Takes the formatted analytics data from the context and writes the data to * the application logs. - * @param {ReportProcessingContext} context the context for the action chain. + * + * @param {import('../report_processing_context')} context the context for the + * action chain. */ async executeStrategy(context) { context.logger.debug("Logging analytics data"); diff --git a/src/actions/process_google_analytics_results.js b/src/actions/process_google_analytics_results.js index c4abaa15..19e0c20b 100644 --- a/src/actions/process_google_analytics_results.js +++ b/src/actions/process_google_analytics_results.js @@ -7,7 +7,8 @@ class ProcessGoogleAnalyticsResults extends Action { #analyticsDataProcessor; /** - * @param {AnalyticsDataProcessor} analyticsDataProcessor + * @param {import('../process_results/analytics_data_processor')} analyticsDataProcessor + * the AnalyticsDataProcessor instance */ constructor(analyticsDataProcessor) { super(); @@ -18,7 +19,9 @@ class ProcessGoogleAnalyticsResults extends Action { * Takes the raw analytics data from the context, processes it to a flatter * object schema, and adds totalling based on report config options. Writes * the processed data to the context for use in subsequent actions. - * @param {ReportProcessingContext} context the context for the action chain. + * + * @param {import('../report_processing_context')} context the context for the + * action chain. */ async executeStrategy(context) { context.logger.debug("Processing GA report data"); diff --git a/src/actions/publish_analytics_data_to_disk.js b/src/actions/publish_analytics_data_to_disk.js index c3eb2adf..2750ca44 100644 --- a/src/actions/publish_analytics_data_to_disk.js +++ b/src/actions/publish_analytics_data_to_disk.js @@ -6,8 +6,9 @@ const DiskPublisher = require("../publish/disk"); */ class PublishAnalyticsDataToDisk extends Action { /** - * @param {ReportProcessingContext} context the context for the action chain. - * @returns {Boolean} true if the application config is set to publish data to + * @param {import('../report_processing_context')} context the context for the + * action chain. + * @returns {boolean} true if the application config is set to publish data to * disk. */ handles(context) { @@ -18,7 +19,9 @@ class PublishAnalyticsDataToDisk extends Action { * Takes the formatted analytics data from the context and writes the data to * disk at a path specified in the application config with the report name as * the filename. - * @param {ReportProcessingContext} context the context for the action chain. + * + * @param {import('../report_processing_context')} context the context for the + * action chain. */ async executeStrategy(context) { context.logger.debug("Publishing analytics data to disk"); diff --git a/src/actions/publish_analytics_data_to_s3.js b/src/actions/publish_analytics_data_to_s3.js index 09bec120..5ca8ca19 100644 --- a/src/actions/publish_analytics_data_to_s3.js +++ b/src/actions/publish_analytics_data_to_s3.js @@ -7,7 +7,7 @@ class PublishAnalyticsDataToS3 extends Action { #s3Service; /** - * @param {S3Service} s3Service + * @param {import('../publish/s3')} s3Service the S3Service instance */ constructor(s3Service) { super(); @@ -15,8 +15,9 @@ class PublishAnalyticsDataToS3 extends Action { } /** - * @param {ReportProcessingContext} context the context for the action chain. - * @returns {Boolean} true if the application config is set to publish data to + * @param {import('../report_processing_context')} context the context for the + * action chain. + * @returns {boolean} true if the application config is set to publish data to * AWS S3. */ handles(context) { @@ -27,7 +28,9 @@ class PublishAnalyticsDataToS3 extends Action { * Takes the formatted analytics data from the context and writes the data to * AWS S3 in a bucket and path specified in the application config with the * report name as the filename. - * @param {ReportProcessingContext} context the context for the action chain. + * + * @param {import('../report_processing_context')} context the context for the + * action chain. */ async executeStrategy(context) { context.logger.debug("Publishing analytics data to S3"); diff --git a/src/actions/query_google_analytics.js b/src/actions/query_google_analytics.js index acd41686..89e75024 100644 --- a/src/actions/query_google_analytics.js +++ b/src/actions/query_google_analytics.js @@ -8,7 +8,8 @@ class QueryGoogleAnalytics extends Action { #googleAnalyticsService; /** - * @param {GoogleAnalyticsService} googleAnalyticsService + * @param {import('../google_analytics/service')} googleAnalyticsService the + * google analytics service instance. */ constructor(googleAnalyticsService) { super(); @@ -21,7 +22,8 @@ class QueryGoogleAnalytics extends Action { * passed to the google analytics service to make the API call(s) necessary to * retrieve the data. The analytics data and the query are set to the context * to be used by subsequent actions. - * @param {ReportProcessingContext} context the context for the action chain. + * @param {import('../report_processing_context')} context the context for the + * action chain. */ async executeStrategy(context) { const reportConfig = context.reportConfig; diff --git a/src/actions/write_analytics_data_to_database.js b/src/actions/write_analytics_data_to_database.js index 4ac3a3b5..f8e0f37d 100644 --- a/src/actions/write_analytics_data_to_database.js +++ b/src/actions/write_analytics_data_to_database.js @@ -7,8 +7,8 @@ class WriteAnalyticsDataToDatabase extends Action { #postgresPublisher; /** - * - * @param {PostgresPublisher} postgresPublisher + * @param {import('../publish/postgres')} postgresPublisher the publisher + * instance */ constructor(postgresPublisher) { super(); @@ -16,8 +16,9 @@ class WriteAnalyticsDataToDatabase extends Action { } /** - * @param {ReportProcessingContext} context the context for the action chain. - * @returns {Boolean} true if the application and report config is set to + * @param {import('../report_processing_context')} context the context for the + * action chain. + * @returns {boolean} true if the application and report config is set to * write processed analytics data to the database. */ handles(context) { @@ -29,7 +30,9 @@ class WriteAnalyticsDataToDatabase extends Action { /** * Takes the processed analytics data from the context and writes the data to * the postgres database. - * @param {ReportProcessingContext} context the context for the action chain. + * + * @param {import('../report_processing_context')} context the context for the + * action chain. */ async executeStrategy(context) { context.logger.debug("Writing report data to database"); diff --git a/src/app_config.js b/src/app_config.js index 645662a6..412ec28a 100644 --- a/src/app_config.js +++ b/src/app_config.js @@ -1,28 +1,30 @@ const path = require("path"); const knexfile = require("../knexfile"); -// Application config +/** + * Application config + */ class AppConfig { #options; /** - * @param {Object} options an object with options to be used when processing + * @param {object} options an object with options to be used when processing * all reports. - * @param {String} options.format the format of the analytics data produced. + * @param {string} options.format the format of the analytics data produced. * Accepted formats are "csv" or "json" - * @param {String} options.output a string filepath where the analytics data + * @param {string} options.output a string filepath where the analytics data * will be written to disk after processing. - * @param {Boolean} options.publish if true, the analytics data will be written + * @param {boolean} options.publish if true, the analytics data will be written * to AWS S3 after processing. - * @param {Boolean} options.realtime if true, the application will use the + * @param {boolean} options.realtime if true, the application will use the * google analytics realtime data API when requesting data. - * @param {Boolean} options.slim if true, the application will create a smaller + * @param {boolean} options.slim if true, the application will create a smaller * data object when formatting the processed data. - * @param {Boolean} options['write-to-database'] if true, the application will + * @param {boolean} options.'write-to-database' if true, the application will * write the processed analytics data to the postgres database. - * @param {String} options.only if set, runs only the report with name + * @param {string} options.only if set, runs only the report with name * matching the passed string. - * @param {String} options.frequency if set, runs only the reports with + * @param {string} options.frequency if set, runs only the reports with * frequency matching the passed string. */ constructor(options = {}) { @@ -54,21 +56,25 @@ class AppConfig { } /** - * @returns {Boolean} true if report configs with slim:true should have their + * @returns {boolean} true if report configs with slim:true should have their * data removed from report results and only include totals. */ get slim() { return !!this.#options.slim; } - // The number of times to retry GA4 API calls. Defaults to 5 + /** + * @returns {number} The number of times to retry GA4 API calls. Defaults to 5 + */ get ga4CallRetryCount() { return Number.parseInt(process.env.ANALYTICS_GA4_CALL_RETRY_COUNT || 5); } - // The number of milliseconds to delay before retrying a GA4 API call. - // Defaults to 1000. (This is only the first retry delay, subsequent calls - // will use exponential backoff.) + /** + * @returns {number} The number of milliseconds to delay before retrying a GA4 + * API call. Defaults to 1000. (This is only the first retry delay, subsequent + * calls will use exponential backoff.) + */ get ga4CallRetryDelay() { return Number.parseInt( process.env.ANALYTICS_GA4_CALL_RETRY_DELAY_MS || 1000, @@ -91,8 +97,8 @@ class AppConfig { return process.env.ANALYTICS_LOG_LEVEL || "debug"; } - // TODO: This doesn't seem to be used. get key() { + // TODO: This doesn't seem to be used. return process.env.ANALYTICS_KEY; } @@ -107,17 +113,17 @@ class AppConfig { return process.env.ANALYTICS_CREDENTIALS; } - // TODO: This seems to be unused get debug() { + // TODO: This seems to be unused return !!process.env.ANALYTICS_DEBUG; } - /* - AWS S3 information. - - Separately, you need to set AWS_REGION, AWS_ACCESS_KEY_ID, and - AWS_SECRET_ACCESS_KEY. The AWS SDK for Node reads these in automatically. - */ + /** + * AWS S3 information. + * Separately, you need to set AWS_REGION, AWS_ACCESS_KEY_ID, and + * AWS_SECRET_ACCESS_KEY. The AWS SDK for Node reads these in automatically. + * @returns {object} the AWS config object + */ get aws() { if (this.#isCloudGov) { return this.#cloudGovAwsConfig; @@ -205,5 +211,4 @@ class AppConfig { } } -// Set environment variables to configure the application. module.exports = AppConfig; diff --git a/src/google_analytics/credential_loader.js b/src/google_analytics/credential_loader.js index ab9d27de..35071837 100644 --- a/src/google_analytics/credential_loader.js +++ b/src/google_analytics/credential_loader.js @@ -10,8 +10,8 @@ class GoogleAnalyticsCredentialLoader { /** * Gets google analytics credentials based on settings in the Config class. * - * @param {AppConfig} appConfig an instance of the application config class - * @returns {Object} a google analytics credential object with "email" and + * @param {import('../app_config')} appConfig an instance of the application config class + * @returns {object} a google analytics credential object with "email" and * "key" properties * @throws {Error} if no credentials are set in the application config */ @@ -19,15 +19,15 @@ class GoogleAnalyticsCredentialLoader { if (appConfig.key) { return { key: appConfig.key, email: appConfig.email }; } else if (appConfig.key_file) { - return this._loadCredentialsFromKeyfile(appConfig); + return this.#loadCredentialsFromKeyfile(appConfig); } else if (appConfig.analytics_credentials) { - return this._loadCredentialsFromEnvironment(appConfig); + return this.#loadCredentialsFromEnvironment(appConfig); } else { throw new Error("No key or key file specified in appConfig"); } } - static _loadCredentialsFromKeyfile(appConfig) { + static #loadCredentialsFromKeyfile(appConfig) { const keyfile = appConfig.key_file; if (!fs.existsSync(keyfile)) { throw new Error(`No such key file: ${keyfile}`); @@ -44,16 +44,16 @@ class GoogleAnalyticsCredentialLoader { return { key, email }; } - static _loadCredentialsFromEnvironment(appConfig) { + static #loadCredentialsFromEnvironment(appConfig) { const credentialData = JSON.parse( Buffer.from(appConfig.analytics_credentials, "base64").toString("utf8"), ); - const credentialsArray = this._wrapArray(credentialData); + const credentialsArray = this.#wrapArray(credentialData); const index = this.analyticsCredentialsIndex++ % credentialsArray.length; return credentialsArray[index]; } - static _wrapArray(object) { + static #wrapArray(object) { return Array.isArray(object) ? object : [object]; } } diff --git a/src/google_analytics/query_authorizer.js b/src/google_analytics/query_authorizer.js index c226dac8..d15877fe 100644 --- a/src/google_analytics/query_authorizer.js +++ b/src/google_analytics/query_authorizer.js @@ -6,10 +6,10 @@ const GoogleAnalyticsCredentialLoader = require("./credential_loader"); */ class GoogleAnalyticsQueryAuthorizer { /** - * @param {Object} query the query object for the google analytics reporting + * @param {object} query the query object for the google analytics reporting * API. - * @param {AppConfig} appConfig an application config instance. - * @returns {Object} the query object with current authorization JWT included. + * @param {import('../app_config')} appConfig an application config instance. + * @returns {object} the query object with current authorization JWT included. */ static authorizeQuery(query, appConfig) { const credentials = diff --git a/src/google_analytics/query_builder.js b/src/google_analytics/query_builder.js index 559b9cde..ee315eec 100644 --- a/src/google_analytics/query_builder.js +++ b/src/google_analytics/query_builder.js @@ -1,7 +1,7 @@ /** - * @param {Object} reportConfig report config for the query to be created. - * @param {AppConfig} appConfig application config instance. - * @returns {Object} an object in the correct syntax for the google analytics + * @param {object} reportConfig report config for the query to be created. + * @param {import('../app_config')} appConfig application config instance. + * @returns {object} an object in the correct syntax for the google analytics * reporting API to execute. */ const buildQuery = (reportConfig, appConfig) => { diff --git a/src/google_analytics/service.js b/src/google_analytics/service.js index bf536230..6b858964 100644 --- a/src/google_analytics/service.js +++ b/src/google_analytics/service.js @@ -10,12 +10,12 @@ class GoogleAnalyticsService { #logger; /** - * @param {BetaAnalyticsDataClient} analyticsDataClient the client for Google - * Analytics Data API operations. - * @param {AppConfig} appConfig application config instance. Provides the + * @param {import('@google-analytics/data').BetaAnalyticsDataClient} analyticsDataClient + * the client for Google Analytics Data API operations. + * @param {import('../app_config')} appConfig application config instance. Provides the * configuration to create an S3 client and the file extension to use for * write operations. - * @param {winston.Logger} logger a logger instance. + * @param {import('winston').Logger} logger a logger instance. */ constructor(analyticsDataClient, appConfig, logger) { this.#analyticsDataClient = analyticsDataClient; @@ -27,11 +27,11 @@ class GoogleAnalyticsService { * Runs a GA report request. Retries with exponential backoff to help prevent * the app failing calls due to API rate limiting. * - * @param {Object} query the report to run in the format required by the GA4 + * @param {object} query the report to run in the format required by the GA4 * reporting API. - * @param {Boolean} isRealtime true if the report should use the realtime + * @param {boolean} isRealtime true if the report should use the realtime * report function - * @returns {Object} the results of the GA4 report API call. + * @returns {object} the results of the GA4 report API call. */ async runReportQuery(query, isRealtime = false) { const authorizedQuery = await this.#authorizeQuery(query); diff --git a/src/logger.js b/src/logger.js index 5b0998fb..b0b2267b 100644 --- a/src/logger.js +++ b/src/logger.js @@ -1,9 +1,11 @@ const winston = require("winston"); /** - * @param {AppConfig} appConfig the application config - * @param {String} reportName the name of the report currently being processed - * @returns {String} a standard tag for the logger to identify the specific + * @param {import('../app_config')} appConfig the application config + * @param {object} reportConfig the name of the report currently being processed + * @param {string} reportConfig.name the name of the report being run for this + * logger instance. + * @returns {string} a standard tag for the logger to identify the specific * report being processed when writing logs. */ const tag = (appConfig, reportConfig) => { @@ -26,13 +28,13 @@ module.exports = { /** * Creates an application logger instance. * - * @param {AppConfig} appConfig application config instance. Sets the log level and + * @param {import('../app_config')} appConfig application config instance. Sets the log level and * is also referenced to create a leading log tag for this logger instance. - * @param {Object} reportConfig config for the report being run for this + * @param {object} reportConfig config for the report being run for this * logger instance. Used to create a leading log tag for messages - * @param {String} reportConfig.name the name of the report being run for this + * @param {string} reportConfig.name the name of the report being run for this * logger instance. Used to create a leading log tag for messages - * @returns {winston.Logger} the configured logger instance + * @returns {import('winston').Logger} the configured logger instance */ initialize: (appConfig = { logLevel: "debug" }, reportConfig = {}) => { return winston.createLogger({ diff --git a/src/process_results/analytics_data_processor.js b/src/process_results/analytics_data_processor.js index 09802ccd..9d4e107a 100644 --- a/src/process_results/analytics_data_processor.js +++ b/src/process_results/analytics_data_processor.js @@ -36,7 +36,7 @@ class AnalyticsDataProcessor { }; /** - * @param {AppConfig} appConfig an instance of the application config class. + * @param {import('../app_config')} appConfig an instance of the application config class. * Provides agency and hostname string values. */ constructor(appConfig) { @@ -45,10 +45,10 @@ class AnalyticsDataProcessor { } /** - * @param {Object} report The report object that was requested - * @param {Object} data The response object from the Google Analytics Data API - * @param {Object} query The query object for the report - * @returns {Object} The response data transformed to flatten the data + * @param {object} report The report object that was requested + * @param {object} data The response object from the Google Analytics Data API + * @param {object} query The query object for the report + * @returns {object} The response data transformed to flatten the data * structure, format dates, and map from GA keys to DAP keys. Data is filtered * as requested in the report object. This object also includes details from * the original report and query. @@ -131,6 +131,10 @@ class AnalyticsDataProcessor { * If dimension or metric is found matching the provided name, then return an * object with rowKey matching the key in row where the value can be found and * index of the named value. If no match is found, return null. + * + * @param {string} name the name of the dimension or metric + * @param {object} data the data row for which to find a header + * @returns {number} the index of the dimension or metric in the values array */ #findDimensionOrMetricIndex(name, data) { const dimensionIndex = data.dimensionHeaders.findIndex((header) => { diff --git a/src/process_results/result_formatter.js b/src/process_results/result_formatter.js index 3f2597d7..1109a123 100644 --- a/src/process_results/result_formatter.js +++ b/src/process_results/result_formatter.js @@ -1,12 +1,12 @@ const csv = require("fast-csv"); /** - * @param {Object} result an analytics object to be formatted. - * @param {Object} config optional configuration for the formatter. - * @param {String} config.format the format to output can be "json" or "csv" - * @param {Boolean} config.slim whether the result should have it's data field + * @param {object} result an analytics object to be formatted. + * @param {object} config optional configuration for the formatter. + * @param {string} config.format the format to output can be "json" or "csv" + * @param {boolean} config.slim whether the result should have it's data field * removed from the result of formatting (only for JSON format). - * @returns {String} a JSON string or a CSV string depending on passed params. + * @returns {string} a JSON string or a CSV string depending on passed params. */ const formatResult = (result, { format = "json", slim = false } = {}) => { result = Object.assign({}, result); diff --git a/src/process_results/result_totals_calculator.js b/src/process_results/result_totals_calculator.js index 49d322fd..12bb1404 100644 --- a/src/process_results/result_totals_calculator.js +++ b/src/process_results/result_totals_calculator.js @@ -1,10 +1,10 @@ /** - * @param {Object} result the result of the analytics report API call after + * @param {object} result the result of the analytics report API call after * processing by the AnalyticsDataProcessor. - * @param {Object} options options for the ResultTotalsCalculator. - * @param {String[]} options.sumVisitsByColumns an array of columns to be + * @param {object} options options for the ResultTotalsCalculator. + * @param {string[]} options.sumVisitsByColumns an array of columns to be * totalled by the number of visits for each unique key in the column. - * @returns {Object} totals for the results. + * @returns {object} totals for the results. */ const calculateTotals = (result, options = {}) => { if (result.data.length === 0) { diff --git a/src/processor.js b/src/processor.js index 7ba591e0..d26f4571 100644 --- a/src/processor.js +++ b/src/processor.js @@ -1,8 +1,12 @@ +/** + * Handles processing a chain of actions + */ class Processor { #actions; /** - * @param {Action[]} actions the chain of actions for the processor + * @param {import('../actions/action')[]} actions the chain of actions for the + * processor. */ constructor(actions = []) { this.#actions = actions; @@ -11,7 +15,7 @@ class Processor { /** * Process a chain of actions with a shared context. * - * @param {ReportProcessingContext} context the shared context. + * @param {import('./report_processing_context')} context the shared context. */ async processChain(context) { for (const action of this.#actions) { diff --git a/src/publish/disk.js b/src/publish/disk.js index 5f4a9bd4..4a8ebe1c 100644 --- a/src/publish/disk.js +++ b/src/publish/disk.js @@ -4,9 +4,9 @@ const path = require("path"); /** * Publishes report data to a file on the local filesystem. * - * @param {String} name the name of the file to create. - * @param {String} data the data to write to the file. - * @param {AppConfig} appConfig application config instance. Sets the file + * @param {string} name the name of the file to create. + * @param {string} data the data to write to the file. + * @param {import('../app_config')} appConfig application config instance. Sets the file * extension and the path of the file to create. */ const publish = async ({ name }, data, appConfig) => { diff --git a/src/publish/postgres.js b/src/publish/postgres.js index ba25dd6e..62ffb912 100644 --- a/src/publish/postgres.js +++ b/src/publish/postgres.js @@ -12,7 +12,7 @@ class PostgresPublisher { #connectionConfig; /** - * @param {AppConfig} appConfig application config instance. Provides the + * @param {import('../app_config')} appConfig application config instance. Provides the * configuration to create a database connection. */ constructor(appConfig) { @@ -20,9 +20,8 @@ class PostgresPublisher { } /** - * - * @param {Object} results the processed results of analytics report data. - * @param {Object[]} results.data an array of data points to write to the + * @param {object} results the processed results of analytics report data. + * @param {object[]} results.data an array of data points to write to the * Postgres database. * @returns {Promise} resolves when the database operations complete. Rejects * if database operations have an error. diff --git a/src/publish/s3.js b/src/publish/s3.js index 93c3668e..873c1df9 100644 --- a/src/publish/s3.js +++ b/src/publish/s3.js @@ -8,10 +8,9 @@ const zlib = require("zlib"); */ class S3Service { #appConfig; - #s3Client; /** - * @param {AppConfig} appConfig application config instance. Provides the + * @param {import('../app_config')} appConfig application config instance. Provides the * configuration to create an S3 client and the file extension to use for * write operations. */ @@ -46,9 +45,8 @@ class S3Service { /** * Writes analytics data to a file in an S3 bucket. - * - * @param {String} name the name of the file to write. - * @param {Object[]} data an array of data points to write to the + * @param {string} name the name of the file to write. + * @param {object[]} data an array of data points to write to the * S3 bucket. * @returns {Promise} resolves when the S3 operations complete. Rejects * if S3 operations have an error. diff --git a/src/report_processing_context.js b/src/report_processing_context.js index 25940dd7..15782d6b 100644 --- a/src/report_processing_context.js +++ b/src/report_processing_context.js @@ -8,7 +8,8 @@ class ReportProcessingContext { #asyncLocalStorage; /** - * @param {AsyncLocalStorage} asyncLocalStorage the storage instance for + * @param {import('node:async_hooks').AsyncLocalStorage} asyncLocalStorage the + * storage instance which a data store for the context. */ constructor(asyncLocalStorage) { this.#asyncLocalStorage = asyncLocalStorage; @@ -17,9 +18,10 @@ class ReportProcessingContext { /** * Begins an async function where the AsyncLocalStorage instance's store holds * the data for the async function's life. + * * @param {Function} asyncFunction the async function for the class to provide * context data. - * @returns the result of the asyncFunction + * @returns {Promise} the result of the asyncFunction */ run(asyncFunction) { const store = new Map(); diff --git a/test/actions/action.test.js b/test/actions/action.test.js index df21550e..fff9c4d5 100644 --- a/test/actions/action.test.js +++ b/test/actions/action.test.js @@ -10,8 +10,16 @@ describe("Action", () => { subject = new Action(); }); - it("returns true", () => { - expect(subject.handles()).to.equal(true); + describe("when a context is passed", () => { + it("returns true", () => { + expect(subject.handles({})).to.equal(true); + }); + }); + + describe("when a context is passed", () => { + it("returns false", () => { + expect(subject.handles()).to.equal(false); + }); }); }); From 6c1a019798426682f208049bce790bff7bd76ff8 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Tue, 16 Jul 2024 11:49:10 -0400 Subject: [PATCH 2/2] [Security] NPM Audit fixes --- package-lock.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf171730..c1dd26ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2570,9 +2570,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.7.tgz", - "integrity": "sha512-yMaA/cIsRhGzW3ymCNpdlPcInXcovztlgu/rirThj2b87u3RzWUszliOqZ/pldy7yhmJPS8uwog+kZSTa4A0PQ==", + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", "dependencies": { "@grpc/proto-loader": "^0.7.8", "@types/node": ">=12.12.47" @@ -4339,12 +4339,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -5532,9 +5532,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -9892,9 +9892,9 @@ } }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "optional": true, "engines": { "node": ">=10.0.0"