diff --git a/.circleci/config.yml b/.circleci/config.yml.old similarity index 100% rename from .circleci/config.yml rename to .circleci/config.yml.old diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..ab972314 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,112 @@ +on: + push: + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Code checkout + uses: actions/checkout@v4 + - name: Install node + uses: actions/setup-node@v4 + with: + node-version: "lts/*" + cache: 'npm' + - name: Install node dependencies + run: npm ci + - name: Lint javascript + run: npm run lint + test: + needs: lint + runs-on: ubuntu-latest + # Start Postgres as a service, wait until healthy. Uses latest Postgres version. + services: + postgres: + image: postgres:latest + env: + POSTGRES_DB: analytics_reporter_test + POSTGRES_USER: analytics + POSTGRES_PASSWORD: 123abc + ports: + - 5432:5432 + options: + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - name: Code checkout + uses: actions/checkout@v4 + - name: Install node + uses: actions/setup-node@v4 + with: + node-version: "lts/*" + cache: 'npm' + - name: Install node dependencies + run: npm ci + - name: Run tests + run: npm test + deploy_dev: + needs: + - lint + - test + if: github.ref == 'refs/heads/develop' + uses: 18F/analytics-reporter/.github/workflows/deploy.yml@develop + with: + ANALYTICS_KEY_FILE_NAME: ${{ vars.ANALYTICS_KEY_FILE_NAME }} + ANALYTICS_REPORT_EMAIL: ${{ vars.ANALYTICS_REPORT_EMAIL }} + APP_NAME: ${{ vars.APP_NAME_DEV }} + CF_ORGANIZATION_NAME: ${{ vars.CF_ORGANIZATION_NAME }} + CF_SPACE_NAME: ${{ vars.CF_SPACE_NAME_DEV }} + DB_SERVICE_NAME: ${{ vars.DB_SERVICE_NAME_DEV }} + NEW_RELIC_APP_NAME: ${{ vars.NEW_RELIC_APP_NAME_DEV }} + S3_SERVICE_NAME: ${{ vars.S3_SERVICE_NAME_DEV }} + secrets: + ANALYTICS_CREDENTIALS: ${{ secrets.ANALYTICS_CREDENTIALS }} + CF_USERNAME: ${{ secrets.CF_USERNAME_DEV }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD_DEV }} + GA4_CREDS: ${{ secrets.GA4_CREDS }} + NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY_DEV }} + deploy_stg: + needs: + - lint + - test + if: github.ref == 'refs/heads/staging' + uses: 18F/analytics-reporter/.github/workflows/deploy.yml@develop + with: + ANALYTICS_KEY_FILE_NAME: ${{ vars.ANALYTICS_KEY_FILE_NAME }} + ANALYTICS_REPORT_EMAIL: ${{ vars.ANALYTICS_REPORT_EMAIL }} + APP_NAME: ${{ vars.APP_NAME_STG }} + CF_ORGANIZATION_NAME: ${{ vars.CF_ORGANIZATION_NAME }} + CF_SPACE_NAME: ${{ vars.CF_SPACE_NAME_STG }} + DB_SERVICE_NAME: ${{ vars.DB_SERVICE_NAME_STG }} + NEW_RELIC_APP_NAME: ${{ vars.NEW_RELIC_APP_NAME_STG }} + S3_SERVICE_NAME: ${{ vars.S3_SERVICE_NAME_STG }} + secrets: + ANALYTICS_CREDENTIALS: ${{ secrets.ANALYTICS_CREDENTIALS }} + CF_USERNAME: ${{ secrets.CF_USERNAME_STG }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD_STG }} + GA4_CREDS: ${{ secrets.GA4_CREDS }} + NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY_STG }} + deploy_prd: + needs: + - lint + - test + if: github.ref == 'refs/heads/master' + uses: 18F/analytics-reporter/.github/workflows/deploy.yml@develop + with: + ANALYTICS_KEY_FILE_NAME: ${{ vars.ANALYTICS_KEY_FILE_NAME }} + ANALYTICS_REPORT_EMAIL: ${{ vars.ANALYTICS_REPORT_EMAIL }} + APP_NAME: ${{ vars.APP_NAME_PRD }} + CF_ORGANIZATION_NAME: ${{ vars.CF_ORGANIZATION_NAME }} + CF_SPACE_NAME: ${{ vars.CF_SPACE_NAME_PRD }} + DB_SERVICE_NAME: ${{ vars.DB_SERVICE_NAME_PRD }} + NEW_RELIC_APP_NAME: ${{ vars.NEW_RELIC_APP_NAME_PRD }} + S3_SERVICE_NAME: ${{ vars.S3_SERVICE_NAME_PRD }} + secrets: + ANALYTICS_CREDENTIALS: ${{ secrets.ANALYTICS_CREDENTIALS }} + CF_USERNAME: ${{ secrets.CF_USERNAME_PRD }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD_PRD }} + GA4_CREDS: ${{ secrets.GA4_CREDS }} + NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY_PRD }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..cdde3260 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,91 @@ +on: + workflow_call: + inputs: + ANALYTICS_KEY_FILE_NAME: + required: true + type: string + ANALYTICS_REPORT_EMAIL: + required: true + type: string + APP_NAME: + required: true + type: string + CF_ORGANIZATION_NAME: + required: true + type: string + CF_SPACE_NAME: + required: true + type: string + DB_SERVICE_NAME: + required: true + type: string + NEW_RELIC_APP_NAME: + type: string + S3_SERVICE_NAME: + required: true + type: string + secrets: + ANALYTICS_CREDENTIALS: + required: true + CF_USERNAME: + required: true + CF_PASSWORD: + required: true + GA4_CREDS: + required: true + NEW_RELIC_LICENSE_KEY: + +env: + ANALYTICS_CREDENTIALS: ${{ secrets.ANALYTICS_CREDENTIALS }} + ANALYTICS_KEY_FILE_NAME: ${{ inputs.ANALYTICS_KEY_FILE_NAME }} + ANALYTICS_REPORT_EMAIL: ${{ inputs.ANALYTICS_REPORT_EMAIL }} + APP_NAME: ${{ inputs.APP_NAME }} + CF_USERNAME: ${{ secrets.CF_USERNAME }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + CF_ORGANIZATION_NAME: ${{ inputs.CF_ORGANIZATION_NAME }} + CF_SPACE_NAME: ${{ inputs.CF_SPACE_NAME }} + DB_SERVICE_NAME: ${{ inputs.DB_SERVICE_NAME }} + GA4_CREDS: ${{ secrets.GA4_CREDS }} + NEW_RELIC_APP_NAME: ${{ inputs.NEW_RELIC_APP_NAME }} + NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }} + S3_SERVICE_NAME: ${{ inputs.S3_SERVICE_NAME }} + +jobs: + deploy_reporter: + runs-on: ubuntu-latest + steps: + - name: Code checkout + uses: actions/checkout@v4 + - name: Install node + uses: actions/setup-node@v4 + with: + node-version: "lts/*" + cache: 'npm' + - name: Install node dependencies + run: npm ci + - name: Install cloud foundry CLI for interacting with cloud.gov + run: | + sudo curl -v -L -o cf8-cli-installer_8.7.4_x86-64.deb 'https://packages.cloudfoundry.org/stable?release=debian64&version=8.7.4' + sudo dpkg -i cf8-cli-installer_8.7.4_x86-64.deb + - name: Write Google GA4 Credentials file from the value in GA4_CREDS env var. + run: | + echo $GA4_CREDS > ./my-analytics-ga4-65057af58daa.json + - name: Run envsubst on manifest.yml to set environment specific values + run: | + mv manifest.yml manifest.yml.src + envsubst < manifest.yml.src > manifest.yml + cat manifest.yml + - name: Replace config.js and knexfile.js with .cloudgov versions of those files + run: | + rm ./src/config.js + mv ./src/config.cloudgov.js ./src/config.js + rm knexfile.js + mv knexfile.cloudgov.js knexfile.js + - name: Login to cloud.gov and deploy + run: | + set -e + # Log into cloud.gov + cf api api.fr.cloud.gov + cf login -u $CF_USERNAME -p $CF_PASSWORD -o $CF_ORGANIZATION_NAME -s $CF_SPACE_NAME + cf push -f "./manifest.yml" --strategy rolling + cf logout diff --git a/.gitignore b/.gitignore index 89956185..28ee7859 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ npm-debug.log *.swp .git +/.nyc_output # Track selected JSON files !package.json diff --git a/README.md b/README.md index 542235cb..12a7039c 100644 --- a/README.md +++ b/README.md @@ -418,6 +418,21 @@ Compose: docker-compose up ``` +## Linting + +This repo uses Eslint and Prettier for code static analysis and formatting. Run +the linter with: + +```shell +npm run lint +``` + +Automatically fix lint issues with: + +```shell +npm run lint:fix +``` + ## Running the unit tests The unit tests for this repo require a local PostgreSQL database. You can run a @@ -441,6 +456,16 @@ Run the tests (pre-test hook runs DB migrations): npm test ``` +### Running the unit tests with code coverage reporting + +If you wish to see a code coverage report after running the tests, use the +following command. This runs the DB migrations, tests, and the NYC code coverage +tool: + +```shell +npm run coverage +``` + ## Public domain This project is in the worldwide [public domain](LICENSE.md). As stated in [CONTRIBUTING](CONTRIBUTING.md): diff --git a/deploy/cron.js b/deploy/cron.js index deb8a818..f114d39d 100644 --- a/deploy/cron.js +++ b/deploy/cron.js @@ -7,7 +7,7 @@ if (process.env.NEW_RELIC_APP_NAME) { } const spawn = require("child_process").spawn; -const logger = require('../src/logger').initialize(); +const logger = require("../src/logger").initialize(); logger.info("==================================="); logger.info("=== STARTING ANALYTICS-REPORTER ==="); @@ -25,41 +25,41 @@ const runScriptWithLogName = (scriptPath, scriptLoggingName) => { childProcess.stdout.on("data", (data) => { logger.info(`[${scriptLoggingName}]`); // Writes logging output from child processes to console. - console.log(data.toString().trim()) + console.log(data.toString().trim()); }); childProcess.stderr.on("data", (data) => { logger.error(`[${scriptLoggingName}]`); // Writes error logging output from child processes to console. - console.log(data.toString().trim()) + console.log(data.toString().trim()); }); - childProcess.on("exit", (code, signal) => { + childProcess.on("close", (code, signal) => { logger.info(`${scriptLoggingName} exitted with code: ${code}`); if (signal) { logger.info(`${scriptLoggingName} received signal: ${signal}`); } }); -} +}; const api_ua_run = () => { - runScriptWithLogName(`${scriptUARootPath}/api.sh`, 'ua - api.sh') + runScriptWithLogName(`${scriptUARootPath}/api.sh`, "ua - api.sh"); }; const api_run = () => { - runScriptWithLogName(`${scriptRootPath}/api.sh`, 'api.sh') + runScriptWithLogName(`${scriptRootPath}/api.sh`, "api.sh"); }; const daily_run = () => { - runScriptWithLogName(`${scriptRootPath}/daily.sh`, 'daily.sh') + runScriptWithLogName(`${scriptRootPath}/daily.sh`, "daily.sh"); }; const hourly_run = () => { - runScriptWithLogName(`${scriptRootPath}/hourly.sh`, 'hourly.sh') + runScriptWithLogName(`${scriptRootPath}/hourly.sh`, "hourly.sh"); }; const realtime_run = () => { - runScriptWithLogName(`${scriptRootPath}/realtime.sh`, 'realtime.sh') + runScriptWithLogName(`${scriptRootPath}/realtime.sh`, "realtime.sh"); }; /** @@ -72,7 +72,7 @@ const calculateNextDailyRunTimeOffset = () => { currentTime.getFullYear(), currentTime.getMonth(), currentTime.getDate() + 1, - 10 - currentTime.getTimezoneOffset() / 60 + 10 - currentTime.getTimezoneOffset() / 60, ); return (nextRunTime - currentTime) % (1000 * 60 * 60 * 24); }; @@ -92,14 +92,15 @@ setTimeout(() => { // Run at 10 AM UTC, then every 24 hours afterwards daily_run(); setInterval(daily_run, 1000 * 60 * 60 * 24); - //api + // API api_run(); setInterval(api_run, 1000 * 60 * 60 * 24); - //ua api + // UA API api_ua_run(); setInterval(api_ua_run, 1000 * 60 * 60 * 24); }, calculateNextDailyRunTimeOffset()); -//hourly +// hourly setInterval(hourly_run, 1000 * 60 * 60); -//realtime -setInterval(realtime_run, 1000 * 60 * 5); +// realtime. Runs every 15 minutes. +// Google updates realtime reports every 30 minutes, so there is some overlap. +setInterval(realtime_run, 1000 * 60 * 15); diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..ea7f507f --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,20 @@ +const { configs: eslintConfigs } = require("@eslint/js"); +const eslintPluginPrettierRecommended = require("eslint-plugin-prettier/recommended"); +const globals = require("globals"); + +module.exports = [ + { + languageOptions: { + globals: { + ...globals.node, + ...globals.mocha, + }, + }, + }, + { + // UA code is deprecated, so don't bother with static analysis rules. + ignores: ["ua/**/*.js"], + ...eslintConfigs.recommended, + }, + eslintPluginPrettierRecommended, +]; diff --git a/index.js b/index.js index ea134e17..162ed367 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,12 @@ -const config = require("./src/config") -const Analytics = require("./src/analytics") -const DiskPublisher = require("./src/publish/disk") -const PostgresPublisher = require("./src/publish/postgres") -const ResultFormatter = require("./src/process-results/result-formatter") -const S3Publisher = require("./src/publish/s3") -const util = require('util'); -const logger = require('./src/logger').initialize(); +const config = require("./src/config"); +const Analytics = require("./src/analytics"); +const DiskPublisher = require("./src/publish/disk"); +const PostgresPublisher = require("./src/publish/postgres"); +const ResultFormatter = require("./src/process-results/result-formatter"); +const S3Publisher = require("./src/publish/s3"); +const util = require("util"); +const Logger = require("./src/logger"); +const logger = Logger.initialize(); async function run(options = {}) { try { @@ -19,13 +20,13 @@ async function run(options = {}) { } function _filterReports({ only, frequency }) { - const reports = Analytics.reports + const reports = Analytics.reports; if (only) { - return reports.filter(report => report.name === only) + return reports.filter((report) => report.name === only); } else if (frequency) { - return reports.filter(report => report.frequency === frequency) + return reports.filter((report) => report.frequency === frequency); } else { - return reports + return reports; } } @@ -36,33 +37,33 @@ async function _runReports(reports, options) { } catch (e) { // Log errors when running a specific report. Do not return the error // so that subsequent reports still run. - logger.error(util.inspect(e)) + logger.error(util.inspect(e)); } } } async function _runReport(report, options) { - const reportOptions = _optionsForReport(report, options) - logger.debug("[" + report.name + "] Fetching..."); + const reportOptions = _optionsForReport(report, options); + logger.debug(`${Logger.tag(report.name)} Fetching...`); try { const analyticsResults = await Analytics.query(report); - logger.debug("[" + report.name + "] Saving report data...") + logger.debug(`${Logger.tag(report.name)} Saving report data...`); if (config.account.agency_name) { - analyticsResults.agency = config.account.agency_name + analyticsResults.agency = config.account.agency_name; } const dbResults = await _writeReportToDatabase( report, analyticsResults, - options + options, ); const formattedResults = await ResultFormatter.formatResult( dbResults, - reportOptions - ) - await _publishReport(report, formattedResults, reportOptions) + reportOptions, + ); + await _publishReport(report, formattedResults, reportOptions); } catch (e) { - logger.error(`[${report.name}] Encountered an error`) + logger.error(`${Logger.tag(report.name)} encountered an error`); throw e; } } @@ -75,25 +76,25 @@ function _optionsForReport(report, options) { realtime: report.realtime, slim: options.slim && report.slim, writeToDatabase: options["write-to-database"], - } + }; } function _writeReportToDatabase(report, result, options) { if (options["write-to-database"] && !report.realtime) { - return PostgresPublisher.publish(result).then(() => result) + return PostgresPublisher.publish(result).then(() => result); } else { - return Promise.resolve(result) + return Promise.resolve(result); } } function _publishReport(report, formattedResult, options) { - logger.debug(`[${report.name}]`, "Publishing...") + logger.debug(`${Logger.tag(report.name)} Publishing...`); if (options.publish) { - return S3Publisher.publish(report, formattedResult, options) - } else if (options.output && typeof (options.output) === "string") { - return DiskPublisher.publish(report, formattedResult, options) + return S3Publisher.publish(report, formattedResult, options); + } else if (options.output && typeof options.output === "string") { + return DiskPublisher.publish(report, formattedResult, options); } else { - console.log(formattedResult) + console.log(formattedResult); } } diff --git a/knexfile.cloudgov.js b/knexfile.cloudgov.js new file mode 100644 index 00000000..fa73dbd9 --- /dev/null +++ b/knexfile.cloudgov.js @@ -0,0 +1,22 @@ +const VCAP_SERVICES_JSON = JSON.parse(process.env.VCAP_SERVICES); + +module.exports = { + production: { + client: "postgresql", + connection: { + host: VCAP_SERVICES_JSON["aws-rds"][0]["credentials"]["host"], + user: VCAP_SERVICES_JSON["aws-rds"][0]["credentials"]["username"], + password: VCAP_SERVICES_JSON["aws-rds"][0]["credentials"]["password"], + database: VCAP_SERVICES_JSON["aws-rds"][0]["credentials"]["db_name"], + port: 5432, + ssl: true, + }, + pool: { + min: 2, + max: 10, + }, + migrations: { + tableName: "knex_migrations", + }, + }, +}; diff --git a/knexfile.js b/knexfile.js index a5966bb4..5f0e2a38 100644 --- a/knexfile.js +++ b/knexfile.js @@ -1,35 +1,35 @@ module.exports = { development: { - client: 'postgresql', + client: "postgresql", connection: { database: process.env.POSTGRES_DATABASE, host: process.env.POSTGRES_HOST, user: process.env.POSTGRES_USER, password: process.env.POSTGRES_PASSWORD, port: 5432, - } + }, }, test: { - client: 'postgresql', + client: "postgresql", connection: { database: process.env.POSTGRES_DATABASE || "analytics_reporter_test", - host: process.env.POSTGRES_HOST || 'localhost', - user: process.env.POSTGRES_USER || 'analytics', - password: process.env.POSTGRES_PASSWORD || '123abc', + host: process.env.POSTGRES_HOST || "localhost", + user: process.env.POSTGRES_USER || "analytics", + password: process.env.POSTGRES_PASSWORD || "123abc", port: 5432, }, migrations: { - tableName: 'knex_migrations', + tableName: "knex_migrations", }, }, production: { - client: 'postgresql', + client: "postgresql", connection: { database: process.env.POSTGRES_DATABASE, host: process.env.POSTGRES_HOST, user: process.env.POSTGRES_USER, password: process.env.POSTGRES_PASSWORD, ssl: true, - } + }, }, -} +}; diff --git a/manifest.yml b/manifest.yml new file mode 100644 index 00000000..dd74b721 --- /dev/null +++ b/manifest.yml @@ -0,0 +1,40 @@ +applications: +- name: ${APP_NAME} + instances: 1 + # 1GB is needed right at startup because many child processes are spawned in + # parallel. This could be reduced if we limited the number of simultaneous + # processes. + memory: 1024M + disk_quota: 1024M + no-route: true + health-check-type: process + buildpacks: + - nodejs_buildpack + command: node deploy/cron.js + env: + # This is a JSON string which has been base64 encoded + ANALYTICS_CREDENTIALS: ${ANALYTICS_CREDENTIALS} + ANALYTICS_DEBUG: 'true' + ANALYTICS_KEY_PATH: /home/vcap/app/${ANALYTICS_KEY_FILE_NAME} + # The default property ID for reports (used for gov-wide reports) + ANALYTICS_REPORT_IDS: '393249053' + # The default property ID for UA reports (used for gov-wide reports) + ANALYTICS_REPORT_UA_IDS: 'ga:96302018' + ANALYTICS_REPORT_EMAIL: ${ANALYTICS_REPORT_EMAIL} + ANALYTICS_REPORTS_PATH: /home/vcap/app/reports/usa.json + ANALYTICS_ROOT_PATH: /home/vcap/app + ANALYTICS_UA_ROOT_PATH: /home/vcap/app/ua + # The default path for reports (used for gov-wide reports) + AWS_BUCKET_PATH: data/live + AWS_CACHE_TIME: '0' + GOOGLE_APPLICATION_CREDENTIALS: /home/vcap/app/${ANALYTICS_KEY_FILE_NAME} + NEW_RELIC_APP_NAME: ${NEW_RELIC_APP_NAME} + NEW_RELIC_LICENSE_KEY: ${NEW_RELIC_LICENSE_KEY} + NODE_ENV: production + PGSSLMODE: true + services: + - ${S3_SERVICE_NAME} + - ${DB_SERVICE_NAME} + stack: cflinuxfs4 + timeout: 180 + path: . diff --git a/migrations/20170308164751_create_analytics_data.js b/migrations/20170308164751_create_analytics_data.js index 12f4c0ca..8ae0abf2 100644 --- a/migrations/20170308164751_create_analytics_data.js +++ b/migrations/20170308164751_create_analytics_data.js @@ -1,14 +1,14 @@ -exports.up = function(knex) { - return knex.schema.createTable("analytics_data", table => { - table.increments("id") - table.string("report_name") - table.string("report_agency") - table.dateTime("date_time") - table.jsonb("data") - table.timestamps(true, true) - }) +exports.up = function (knex) { + return knex.schema.createTable("analytics_data", (table) => { + table.increments("id"); + table.string("report_name"); + table.string("report_agency"); + table.dateTime("date_time"); + table.jsonb("data"); + table.timestamps(true, true); + }); }; -exports.down = function(knex) { - return knex.schema.dropTable('analytics_data') +exports.down = function (knex) { + return knex.schema.dropTable("analytics_data"); }; diff --git a/migrations/20170316115145_add_analytics_data_indexes.js b/migrations/20170316115145_add_analytics_data_indexes.js index 19e4fe27..82a9c8c9 100644 --- a/migrations/20170316115145_add_analytics_data_indexes.js +++ b/migrations/20170316115145_add_analytics_data_indexes.js @@ -1,14 +1,18 @@ -exports.up = function(knex) { - return knex.schema.table("analytics_data", table => { - table.index(["report_name", "report_agency"]) - }).then(() => { - return knex.schema.raw("CREATE INDEX analytics_data_date_time_desc ON analytics_data (date_time DESC NULLS LAST)") - }) +exports.up = function (knex) { + return knex.schema + .table("analytics_data", (table) => { + table.index(["report_name", "report_agency"]); + }) + .then(() => { + return knex.schema.raw( + "CREATE INDEX analytics_data_date_time_desc ON analytics_data (date_time DESC NULLS LAST)", + ); + }); }; -exports.down = function(knex, Promise) { - return knex.schema.table("analytics_data", table => { - table.dropIndex(["report_name", "report_agency"]) - table.dropIndex("date_time", "analytics_data_date_time_desc") - }) +exports.down = function (knex) { + return knex.schema.table("analytics_data", (table) => { + table.dropIndex(["report_name", "report_agency"]); + table.dropIndex("date_time", "analytics_data_date_time_desc"); + }); }; diff --git a/migrations/20170522094056_rename_date_time_to_date.js b/migrations/20170522094056_rename_date_time_to_date.js index 519eace8..4ab221d4 100644 --- a/migrations/20170522094056_rename_date_time_to_date.js +++ b/migrations/20170522094056_rename_date_time_to_date.js @@ -1,12 +1,19 @@ - -exports.up = function(knex, Promise) { - return knex.schema.raw("ALTER TABLE analytics_data RENAME COLUMN date_time TO date").then(() => { - return knex.schema.raw("ALTER TABLE analytics_data ALTER COLUMN date TYPE date") - }) +exports.up = function (knex) { + return knex.schema + .raw("ALTER TABLE analytics_data RENAME COLUMN date_time TO date") + .then(() => { + return knex.schema.raw( + "ALTER TABLE analytics_data ALTER COLUMN date TYPE date", + ); + }); }; -exports.down = function(knex, Promise) { - return knex.schema.raw("ALTER TABLE analytics_data RENAME COLUMN date TO date_time").then(() => { - return knex.schema.raw("ALTER TABLE analytics_data ALTER COLUMN date_time TYPE timestamp with time zone") - }) +exports.down = function (knex) { + return knex.schema + .raw("ALTER TABLE analytics_data RENAME COLUMN date TO date_time") + .then(() => { + return knex.schema.raw( + "ALTER TABLE analytics_data ALTER COLUMN date_time TYPE timestamp with time zone", + ); + }); }; diff --git a/migrations/20210706213753_add_date_id_multi_col_index.js b/migrations/20210706213753_add_date_id_multi_col_index.js index 069f3906..2de7dd1d 100644 --- a/migrations/20210706213753_add_date_id_multi_col_index.js +++ b/migrations/20210706213753_add_date_id_multi_col_index.js @@ -1,11 +1,11 @@ - -exports.up = function(knex) { - return knex.schema.raw("CREATE INDEX analytics_data_date_desc_id_asc ON analytics_data (date DESC NULLS LAST, id ASC)") +exports.up = function (knex) { + return knex.schema.raw( + "CREATE INDEX analytics_data_date_desc_id_asc ON analytics_data (date DESC NULLS LAST, id ASC)", + ); }; -exports.down = function(knex, Promise) { - return knex.schema.table("analytics_data", table => { - table.dropIndex("analytics_data_date_desc_id_asc") - }) +exports.down = function (knex) { + return knex.schema.table("analytics_data", (table) => { + table.dropIndex("analytics_data_date_desc_id_asc"); + }); }; - diff --git a/migrations/20231218165411_create_analytics_data_ga4.js b/migrations/20231218165411_create_analytics_data_ga4.js index c2f5a3af..2da7f16c 100644 --- a/migrations/20231218165411_create_analytics_data_ga4.js +++ b/migrations/20231218165411_create_analytics_data_ga4.js @@ -1,15 +1,15 @@ -exports.up = function(knex) { - return knex.schema.createTable("analytics_data_ga4", table => { - table.increments("id") - table.string("report_name") - table.string("report_agency") - table.dateTime("date") - table.jsonb("data") - table.timestamps(true, true) - table.string("version") - }) +exports.up = function (knex) { + return knex.schema.createTable("analytics_data_ga4", (table) => { + table.increments("id"); + table.string("report_name"); + table.string("report_agency"); + table.dateTime("date"); + table.jsonb("data"); + table.timestamps(true, true); + table.string("version"); + }); }; -exports.down = function(knex) { - return knex.schema.dropTable('analytics_data_ga4') +exports.down = function (knex) { + return knex.schema.dropTable("analytics_data_ga4"); }; diff --git a/migrations/20240130203237_rename_date_time_to_date_ga4.js b/migrations/20240130203237_rename_date_time_to_date_ga4.js index def01fdd..cca3ced2 100644 --- a/migrations/20240130203237_rename_date_time_to_date_ga4.js +++ b/migrations/20240130203237_rename_date_time_to_date_ga4.js @@ -2,14 +2,18 @@ * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.up = function(knex) { - return knex.schema.raw("ALTER TABLE analytics_data_ga4 ALTER COLUMN date TYPE date") +exports.up = function (knex) { + return knex.schema.raw( + "ALTER TABLE analytics_data_ga4 ALTER COLUMN date TYPE date", + ); }; /** * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.down = function(knex) { - return knex.schema.raw("ALTER TABLE analytics_data_ga4 ALTER COLUMN date TYPE timestamp with time zone") +exports.down = function (knex) { + return knex.schema.raw( + "ALTER TABLE analytics_data_ga4 ALTER COLUMN date TYPE timestamp with time zone", + ); }; diff --git a/migrations/20240130203849_remove_version_col_ga4.js b/migrations/20240130203849_remove_version_col_ga4.js index 59279a09..dc7b7706 100644 --- a/migrations/20240130203849_remove_version_col_ga4.js +++ b/migrations/20240130203849_remove_version_col_ga4.js @@ -2,18 +2,18 @@ * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.up = function(knex) { - return knex.schema.table("analytics_data_ga4", table => { - table.dropColumn('version') - }) +exports.up = function (knex) { + return knex.schema.table("analytics_data_ga4", (table) => { + table.dropColumn("version"); + }); }; /** * @param { import("knex").Knex } knex * @returns { Promise } */ -exports.down = function(knex) { - return knex.schema.table("analytics_data_ga4", table => { - table.string('version') - }) +exports.down = function (knex) { + return knex.schema.table("analytics_data_ga4", (table) => { + table.string("version"); + }); }; diff --git a/newrelic.js b/newrelic.js index c162fdb3..f9d50488 100644 --- a/newrelic.js +++ b/newrelic.js @@ -2,6 +2,6 @@ exports.config = { app_name: [process.env.NEW_RELIC_APP_NAME], license_key: process.env.NEW_RELIC_LICENSE_KEY, logging: { - level: "info" + level: "info", }, -} +}; diff --git a/nyc.config.js b/nyc.config.js new file mode 100644 index 00000000..062ff97d --- /dev/null +++ b/nyc.config.js @@ -0,0 +1,25 @@ +"use strict"; + +/** + * Configures the code coverage tool NYC. + */ +module.exports = { + all: true, + exclude: [ + "coverage", + "eslint.config.js", + "knexfile.js", + "knexfile.cloudgov.js", + "migrations", + "newrelic.js", + "nyc.config.js", + "node_modules", + "test", + // Ignore UA because it will be deprecated + "ua", + ], + branches: 100, + functions: 100, + lines: 100, + statements: 100, +}; diff --git a/package-lock.json b/package-lock.json index 796756ff..a624b4b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,8 +22,14 @@ "analytics": "bin/analytics" }, "devDependencies": { + "@eslint/js": "^8.57.0", "chai": "^4.4.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "globals": "^14.0.0", "mocha": "^10.2.0", + "nyc": "^15.1.0", "proxyquire": "^2.1.3" }, "engines": { @@ -35,6 +41,28 @@ "pg": "^8.11.3" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@aws-crypto/crc32": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", @@ -1412,226 +1440,1055 @@ "node": ">=14.0.0" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, "engines": { - "node": ">=0.1.90" + "node": ">=6.9.0" } }, - "node_modules/@contrast/fn-inspect": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@contrast/fn-inspect/-/fn-inspect-3.3.0.tgz", - "integrity": "sha512-iulijoAuhfamXZNWsEy4ORNd8TxqD6aKeMiukDpWSwuRJ3sB+4lOmY2DkP2WwlBpYMmh3k4/7LHP2I925Y2xKQ==", - "hasInstallScript": true, - "optional": true, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "dependencies": { - "nan": "^2.16.0", - "node-gyp-build": "^4.4.0" + "color-convert": "^1.9.0" }, "engines": { - "node": ">=12.13.0" + "node": ">=4" } }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/@fast-csv/format": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", - "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "dependencies": { - "@types/node": "^14.0.1", - "lodash.escaperegexp": "^4.1.2", - "lodash.isboolean": "^3.0.3", - "lodash.isequal": "^4.5.0", - "lodash.isfunction": "^3.0.9", - "lodash.isnil": "^4.0.0" + "color-name": "1.1.3" } }, - "node_modules/@fast-csv/parse": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", - "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", - "dependencies": { - "@types/node": "^14.0.1", - "lodash.escaperegexp": "^4.1.2", - "lodash.groupby": "^4.6.0", - "lodash.isfunction": "^3.0.9", - "lodash.isnil": "^4.0.0", - "lodash.isundefined": "^3.0.1", - "lodash.uniq": "^4.5.0" + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" } }, - "node_modules/@google-analytics/data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@google-analytics/data/-/data-4.0.1.tgz", - "integrity": "sha512-YhxMQuno21LF7W9sLZMPBuxJoRQ/A50kFvGJuZUmD4girm5PyEQDpew6vooUh7GjMcfpvM8b1XIIFk6pyFaBjA==", - "dependencies": { - "google-gax": "^4.0.3" - }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, "engines": { - "node": ">=14.0.0" + "node": ">=4" } }, - "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==", + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "dependencies": { - "@grpc/proto-loader": "^0.7.8", - "@types/node": ">=12.12.47" + "has-flag": "^3.0.0" }, "engines": { - "node": "^8.13.0 || >=10.10.0" + "node": ">=4" } }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", - "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "dev": true, "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.4", - "yargs": "^17.7.2" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" }, "engines": { - "node": ">=6" + "node": ">=6.9.0" } }, - "node_modules/@grpc/proto-loader/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@grpc/proto-loader/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@newrelic/aws-sdk": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@newrelic/aws-sdk/-/aws-sdk-7.0.3.tgz", - "integrity": "sha512-oafBFD+DEAqXTg0w15eEu2rfoedWMS7abyW5CuOJxSb+7OWiFUx9jZPpdFblsYPVNWBehxFdws6/JEio3GklyA==", - "optional": true, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, "engines": { - "node": ">=16.0.0" + "node": ">=6.9.0" } }, - "node_modules/@newrelic/koa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@newrelic/koa/-/koa-8.0.1.tgz", - "integrity": "sha512-GyeZGKPllpUu6gWXRwVP/FlvE9+tU2lOprRiTdoXNM8jdVGL02IfHnvAzrIANoZoUdf3+Vev8NNeCup2Eojcvg==", - "optional": true, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, "engines": { - "node": ">=16.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@newrelic/native-metrics": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@newrelic/native-metrics/-/native-metrics-10.0.1.tgz", - "integrity": "sha512-XJlKF3mCiFS/tZj6C79gdRYj+vQQtFSxbL83MMOVK/N025UHk8Oo8lF1ir7GOWk+Ll2xH4WI/t7i9SqDouXX+g==", - "hasInstallScript": true, - "optional": true, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, "dependencies": { - "https-proxy-agent": "^5.0.1", - "nan": "^2.17.0", - "semver": "^7.5.2" + "@babel/types": "^7.22.5" }, "engines": { - "node": ">=16", - "npm": ">=6" + "node": ">=6.9.0" } }, - "node_modules/@newrelic/security-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@newrelic/security-agent/-/security-agent-0.6.0.tgz", - "integrity": "sha512-a7YsdHX9UthzUd7cfqjI4WDG+yyr9MNUFwr+iyuY2u3zMH0Xe99USlSWWp7nH3hgxIQ1J0JJZ1dC/7wuTHYrHw==", - "optional": true, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, "dependencies": { - "@aws-sdk/client-lambda": "^3.436.0", - "axios": "^1.6.3", - "check-disk-space": "^3.4.0", - "content-type": "^1.0.5", - "fast-safe-stringify": "^2.1.1", - "find-package-json": "^1.2.0", - "hash.js": "^1.1.7", - "html-entities": "^2.3.6", - "is-invalid-path": "^1.0.2", - "js-yaml": "^4.1.0", - "jsonschema": "^1.4.1", - "lodash": "^4.17.21", - "log4js": "^6.9.1", - "pretty-bytes": "^5.6.0", - "request-ip": "^3.3.0", - "ringbufferjs": "^2.0.0", - "semver": "^7.5.4", - "sync-request": "^6.1.0", - "unescape": "^1.0.1", - "unescape-js": "^1.1.4", - "uuid": "^9.0.1", - "ws": "^8.14.2" + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@newrelic/superagent": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@newrelic/superagent/-/superagent-7.0.1.tgz", - "integrity": "sha512-QZlW0VxHSVOXcMAtlkg+Mth0Nz3vFku8rfzTEmoI/pXcckHXGEYuiVUhhboCTD3xTKVgnZRUp9BWF6SOggGUSw==", - "optional": true, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, "engines": { - "node": ">=16.0" + "node": ">=6.9.0" } }, - "node_modules/@prisma/prisma-fmt-wasm": { - "version": "4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085", - "resolved": "https://registry.npmjs.org/@prisma/prisma-fmt-wasm/-/prisma-fmt-wasm-4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085.tgz", - "integrity": "sha512-zYz3rFwPB82mVlHGknAPdnSY/a308dhPOblxQLcZgZTDRtDXOE1MgxoRAys+jekwR4/bm3+rZDPs1xsFMsPZig==", - "optional": true + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", + "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", + "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@contrast/fn-inspect": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@contrast/fn-inspect/-/fn-inspect-3.3.0.tgz", + "integrity": "sha512-iulijoAuhfamXZNWsEy4ORNd8TxqD6aKeMiukDpWSwuRJ3sB+4lOmY2DkP2WwlBpYMmh3k4/7LHP2I925Y2xKQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "nan": "^2.16.0", + "node-gyp-build": "^4.4.0" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "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", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@google-analytics/data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@google-analytics/data/-/data-4.0.1.tgz", + "integrity": "sha512-YhxMQuno21LF7W9sLZMPBuxJoRQ/A50kFvGJuZUmD4girm5PyEQDpew6vooUh7GjMcfpvM8b1XIIFk6pyFaBjA==", + "dependencies": { + "google-gax": "^4.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.24.tgz", + "integrity": "sha512-+VaWXDa6+l6MhflBvVXjIEAzb59nQ2JUK3bwRp2zRpPtU+8TFRy9Gg/5oIcNlkEL5PGlBFGfemUVvIgLnTzq7Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@newrelic/aws-sdk": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@newrelic/aws-sdk/-/aws-sdk-7.0.3.tgz", + "integrity": "sha512-oafBFD+DEAqXTg0w15eEu2rfoedWMS7abyW5CuOJxSb+7OWiFUx9jZPpdFblsYPVNWBehxFdws6/JEio3GklyA==", + "optional": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@newrelic/koa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@newrelic/koa/-/koa-8.0.1.tgz", + "integrity": "sha512-GyeZGKPllpUu6gWXRwVP/FlvE9+tU2lOprRiTdoXNM8jdVGL02IfHnvAzrIANoZoUdf3+Vev8NNeCup2Eojcvg==", + "optional": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@newrelic/native-metrics": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@newrelic/native-metrics/-/native-metrics-10.0.1.tgz", + "integrity": "sha512-XJlKF3mCiFS/tZj6C79gdRYj+vQQtFSxbL83MMOVK/N025UHk8Oo8lF1ir7GOWk+Ll2xH4WI/t7i9SqDouXX+g==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "https-proxy-agent": "^5.0.1", + "nan": "^2.17.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=16", + "npm": ">=6" + } + }, + "node_modules/@newrelic/security-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@newrelic/security-agent/-/security-agent-0.6.0.tgz", + "integrity": "sha512-a7YsdHX9UthzUd7cfqjI4WDG+yyr9MNUFwr+iyuY2u3zMH0Xe99USlSWWp7nH3hgxIQ1J0JJZ1dC/7wuTHYrHw==", + "optional": true, + "dependencies": { + "@aws-sdk/client-lambda": "^3.436.0", + "axios": "^1.6.3", + "check-disk-space": "^3.4.0", + "content-type": "^1.0.5", + "fast-safe-stringify": "^2.1.1", + "find-package-json": "^1.2.0", + "hash.js": "^1.1.7", + "html-entities": "^2.3.6", + "is-invalid-path": "^1.0.2", + "js-yaml": "^4.1.0", + "jsonschema": "^1.4.1", + "lodash": "^4.17.21", + "log4js": "^6.9.1", + "pretty-bytes": "^5.6.0", + "request-ip": "^3.3.0", + "ringbufferjs": "^2.0.0", + "semver": "^7.5.4", + "sync-request": "^6.1.0", + "unescape": "^1.0.1", + "unescape-js": "^1.1.4", + "uuid": "^9.0.1", + "ws": "^8.14.2" + } + }, + "node_modules/@newrelic/superagent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@newrelic/superagent/-/superagent-7.0.1.tgz", + "integrity": "sha512-QZlW0VxHSVOXcMAtlkg+Mth0Nz3vFku8rfzTEmoI/pXcckHXGEYuiVUhhboCTD3xTKVgnZRUp9BWF6SOggGUSw==", + "optional": true, + "engines": { + "node": ">=16.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@prisma/prisma-fmt-wasm": { + "version": "4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085", + "resolved": "https://registry.npmjs.org/@prisma/prisma-fmt-wasm/-/prisma-fmt-wasm-4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085.tgz", + "integrity": "sha512-zYz3rFwPB82mVlHGknAPdnSY/a308dhPOblxQLcZgZTDRtDXOE1MgxoRAys+jekwR4/bm3+rZDPs1xsFMsPZig==", + "optional": true + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", @@ -2412,6 +3269,12 @@ "integrity": "sha512-bYuSNomfn4hu2tPiDN+JZtnzCpSpbJ/PNeulmocDy3xN2X5OkJL65zo6rPZp65cPPhLF9vfT/dgE+RtFRCSxOA==", "optional": true }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -2427,7 +3290,7 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "optional": true, + "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -2444,6 +3307,15 @@ "acorn": "^8" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -2455,6 +3327,35 @@ "node": ">= 6.0.0" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -2499,6 +3400,24 @@ "node": ">= 8" } }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2629,6 +3548,38 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -2649,6 +3600,21 @@ "node": ">=4" } }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", @@ -2662,6 +3628,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -2674,6 +3649,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001591", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz", + "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -2780,6 +3775,15 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "optional": true }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -2873,6 +3877,12 @@ "node": ">=14" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2903,12 +3913,32 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "optional": true }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -2958,6 +3988,27 @@ "node": ">=6" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", @@ -2988,6 +4039,18 @@ "node": ">=0.3.1" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dotenv": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", @@ -3018,6 +4081,12 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/electron-to-chromium": { + "version": "1.4.689", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.689.tgz", + "integrity": "sha512-GatzRKnGPS1go29ep25reM94xxd1Wj8ritU0yRhCJ/tr1Bg8gKnm6R9O/yPOhGQBoLMZ9ezfrpghNaTw97C/PQ==", + "dev": true + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3036,6 +4105,12 @@ "once": "^1.4.0" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3056,13 +4131,259 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "optional": true, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, "node_modules/event-target-shim": { @@ -3102,6 +4423,30 @@ "node": ">=10.0.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -3129,11 +4474,32 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/fill-keys": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", @@ -3159,6 +4525,23 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, "node_modules/find-package-json": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/find-package-json/-/find-package-json-1.2.0.tgz", @@ -3190,11 +4573,25 @@ "flat": "cli.js" } }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/flatted": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "optional": true + "devOptional": true }, "node_modules/fn.name": { "version": "1.1.0", @@ -3221,6 +4618,19 @@ } } }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/form-data": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", @@ -3234,6 +4644,26 @@ "node": ">= 0.12" } }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -3325,6 +4755,15 @@ "node": ">=14" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -3360,7 +4799,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "optional": true, + "devOptional": true, "engines": { "node": ">=8.0.0" } @@ -3434,6 +4873,18 @@ "node": "*" } }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/google-auth-library": { "version": "9.4.1", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.4.1.tgz", @@ -3514,7 +4965,13 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "optional": true + "devOptional": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "node_modules/gtoken": { "version": "7.0.1", @@ -3592,6 +5049,31 @@ "minimalistic-assert": "^1.0.1" } }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -3628,6 +5110,12 @@ ], "optional": true }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/http-basic": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz", @@ -3728,6 +5216,40 @@ "node": ">= 6" } }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/import-in-the-middle": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.7.2.tgz", @@ -3740,6 +5262,24 @@ "module-details-from-path": "^1.0.3" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3855,6 +5395,15 @@ "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", "dev": true }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -3875,6 +5424,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -3887,11 +5442,171 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "optional": true + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "node_modules/js-yaml": { "version": "4.1.0", @@ -3905,6 +5620,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -3913,12 +5640,42 @@ "bignumber.js": "^9.0.0" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "optional": true }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -3956,6 +5713,15 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/knex": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz", @@ -4012,6 +5778,19 @@ "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4043,6 +5822,12 @@ "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, "node_modules/lodash.groupby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", @@ -4073,6 +5858,12 @@ "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -4141,7 +5932,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "optional": true, + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -4149,6 +5940,30 @@ "node": ">=10" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -4281,6 +6096,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/newrelic": { "version": "11.9.0", "resolved": "https://registry.npmjs.org/newrelic/-/newrelic-11.9.0.tgz", @@ -4373,6 +6194,24 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4382,6 +6221,183 @@ "node": ">=0.10.0" } }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/object-hash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", @@ -4414,6 +6430,23 @@ "fn.name": "1.x.x" } }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4444,12 +6477,60 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/packet-reader": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==", "optional": true }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse-cache-control": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", @@ -4474,6 +6555,15 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -4579,6 +6669,12 @@ "split2": "^4.1.0" } }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -4587,8 +6683,72 @@ "engines": { "node": ">=8.6" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/postgres-array": { @@ -4630,6 +6790,43 @@ "node": ">=0.10.0" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -4648,6 +6845,18 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "optional": true }, + "node_modules/process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/promise": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", @@ -4708,6 +6917,15 @@ "resolve": "^1.11.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.11.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", @@ -4722,6 +6940,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -4768,6 +7006,18 @@ "node": ">= 10.13.0" } }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/request-ip": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz", @@ -4796,6 +7046,12 @@ "node": ">=8.6.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -4817,7 +7073,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "optional": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -4836,18 +7092,66 @@ "node": ">=14" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rfdc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", "optional": true }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ringbufferjs": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ringbufferjs/-/ringbufferjs-2.0.0.tgz", "integrity": "sha512-GCOqTzUsTHF7nrqcgtNGAFotXztLgiePpIDpyWZ7R5I02tmfJWV+/yuJc//Hlsd8G+WzI1t/dc2y/w2imDZdog==", "optional": true }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4879,7 +7183,7 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "optional": true, + "devOptional": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -4899,6 +7203,12 @@ "randombytes": "^2.1.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, "node_modules/set-function-length": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", @@ -4914,6 +7224,27 @@ "node": ">= 0.4" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -4927,6 +7258,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -4935,6 +7272,32 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/split2": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", @@ -4944,6 +7307,12 @@ "node": ">= 10.x" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -5017,6 +7386,15 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5089,6 +7467,22 @@ "get-port": "^3.1.0" } }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "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/tarn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", @@ -5113,11 +7507,53 @@ "node": ">=14" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, "node_modules/then-request": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz", @@ -5200,6 +7636,15 @@ "node": ">=8" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5227,6 +7672,18 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -5236,12 +7693,33 @@ "node": ">=4" } }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "optional": true }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/unescape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unescape/-/unescape-1.0.1.tgz", @@ -5272,6 +7750,45 @@ "node": ">= 4.0.0" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/url-template": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", @@ -5308,6 +7825,27 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "node_modules/winston": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz", @@ -5377,6 +7915,18 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "node_modules/ws": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", @@ -5419,7 +7969,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true + "devOptional": true }, "node_modules/yargs": { "version": "16.2.0", diff --git a/package.json b/package.json index c4524a1b..2d096c1d 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,11 @@ "pretest": "NODE_ENV=test npm run migrate", "start": "node app.js", "test": "NODE_ENV=test mocha", + "coverage": "nyc npm run test", "prepare": "npm run snyk-protect", - "snyk-protect": "snyk-protect" + "snyk-protect": "snyk-protect", + "lint": "eslint .", + "lint:fix": "eslint . --fix" }, "contributors": [ { @@ -70,8 +73,14 @@ "winston": "^3.11.0" }, "devDependencies": { + "@eslint/js": "^8.57.0", "chai": "^4.4.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "globals": "^14.0.0", "mocha": "^10.2.0", + "nyc": "^15.1.0", "proxyquire": "^2.1.3" }, "optionalDependencies": { diff --git a/reports/usa.json b/reports/usa.json index 3b1e60c1..b8a3a392 100644 --- a/reports/usa.json +++ b/reports/usa.json @@ -900,21 +900,6 @@ "metrics": [ { "name": "sessions" - }, - { - "name": "totalUsers" - }, - { - "name": "screenPageViews" - }, - { - "name": "screenPageViewsPerSession" - }, - { - "name": "averageSessionDuration" - }, - { - "name": "bounceRate" } ], "dateRanges": [ @@ -966,18 +951,6 @@ } } } - }, - { - "notExpression": { - "filter": { - "fieldName": "unifiedScreenName", - "stringFilter": { - "matchType": "FULL_REGEXP", - "value": "\/(.+)$", - "caseSensitive": false - } - } - } } ] } diff --git a/sample.manifest.yml b/sample.manifest.yml deleted file mode 100644 index 231e5ce8..00000000 --- a/sample.manifest.yml +++ /dev/null @@ -1,36 +0,0 @@ -applications: -- name: analytics-reporter - instances: 1 - memory: 256M - disk_quota: 1024M - no-route: true - health-check-type: process - buildpacks: - - nodejs_buildpack - command: node deploy/cron.js - env: - ANALYTICS_ROOT_PATH: - ANALYTICS_REPORT_IDS: - ANALYTICS_REPORTS_PATH: - AWS_ACCESS_KEY_ID: - AWS_BUCKET: - AWS_BUCKET_PATH: - AWS_CACHE_TIME: - AWS_DEFAULT_REGION: - AWS_REGION: - AWS_SECRET_ACCESS_KEY: - CREDS: - BUCKET_NAME: - PGSSLMODE: true - POSTGRES_DATABASE: - POSTGRES_HOST: - POSTGRES_PASSWORD: - POSTGRES_USER: - NODE_ENV: production - ANALYTICS_DEBUG: true - services: - - analytics-s3 - - analytics-reporter-database - stack: cflinuxfs4 - timeout: 180 - path: . diff --git a/src/analytics.js b/src/analytics.js index c4ec1e4e..476e5c8c 100755 --- a/src/analytics.js +++ b/src/analytics.js @@ -1,26 +1,31 @@ -const path = require("path") -const config = require('./config') -const GoogleAnalyticsClient = require("./google-analytics/client") -const GoogleAnalyticsDataProcessor = require("./process-results/ga-data-processor") -const GoogleAnalyticsQueryBuilder = require("./google-analytics/query-builder") +const path = require("path"); +const config = require("./config"); +const GoogleAnalyticsClient = require("./google-analytics/client"); +const GoogleAnalyticsDataProcessor = require("./process-results/ga-data-processor"); +const GoogleAnalyticsQueryBuilder = require("./google-analytics/query-builder"); const query = (report) => { if (!report) { - return Promise.reject(new Error("Analytics.query missing required argument `report`")) + return Promise.reject( + new Error("Analytics.query missing required argument `report`"), + ); } - return GoogleAnalyticsClient.fetchData(report).then(data => { - const query = GoogleAnalyticsQueryBuilder.buildQuery(report) // included here again because it doesn't get returned with data any longer - return GoogleAnalyticsDataProcessor.processData(report, data[0], query) // data is now an array - }) -} + return GoogleAnalyticsClient.fetchData(report).then((data) => { + const query = GoogleAnalyticsQueryBuilder.buildQuery(report); // included here again because it doesn't get returned with data any longer + return GoogleAnalyticsDataProcessor.processData(report, data[0], query); // data is now an array + }); +}; const _loadReports = () => { - const _reportFilePath = path.resolve(process.cwd(), config.reports_file || "reports/reports.json") - return require(_reportFilePath).reports -} + const _reportFilePath = path.resolve( + process.cwd(), + config.reports_file || "reports/reports.json", + ); + return require(_reportFilePath).reports; +}; module.exports = { query, reports: _loadReports(), -} +}; diff --git a/src/config.cloudgov.js b/src/config.cloudgov.js new file mode 100644 index 00000000..37c01be1 --- /dev/null +++ b/src/config.cloudgov.js @@ -0,0 +1,47 @@ +const knexfile = require("../knexfile"); +const VCAP_SERVICES_JSON = JSON.parse(process.env.VCAP_SERVICES); + +// Set AWS env vars based on VCAP service values. +process.env["AWS_ACCESS_KEY_ID"] = + VCAP_SERVICES_JSON["s3"][0]["credentials"]["access_key_id"]; +process.env["AWS_SECRET_ACCESS_KEY"] = + VCAP_SERVICES_JSON["s3"][0]["credentials"]["secret_access_key"]; +process.env["AWS_REGION"] = + VCAP_SERVICES_JSON["s3"][0]["credentials"]["region"]; + +// Set environment variables to configure the application. +module.exports = { + email: process.env.ANALYTICS_REPORT_EMAIL, + key: process.env.ANALYTICS_KEY, + key_file: + process.env.GOOGLE_APPLICATION_CREDENTIALS || + process.env.ANALYTICS_KEY_PATH, + analytics_credentials: process.env.ANALYTICS_CREDENTIALS, + reports_file: process.env.ANALYTICS_REPORTS_PATH, + debug: process.env.ANALYTICS_DEBUG ? true : false, + // AWS S3 information. + aws: { + bucket: VCAP_SERVICES_JSON["s3"][0]["credentials"]["bucket"], + path: process.env.AWS_BUCKET_PATH, + // HTTP cache time in seconds. Defaults to 0. + cache: process.env.AWS_CACHE_TIME, + endpoint: `https://${VCAP_SERVICES_JSON["s3"][0]["credentials"]["endpoint"]}`, + accessKeyId: VCAP_SERVICES_JSON["s3"][0]["credentials"]["access_key_id"], + secretAccessKey: + VCAP_SERVICES_JSON["s3"][0]["credentials"]["secret_access_key"], + region: VCAP_SERVICES_JSON["s3"][0]["credentials"]["region"], + s3ForcePathStyle: process.env.AWS_S3_FORCE_STYLE_PATH, + signatureVersion: process.env.AWS_SIGNATURE_VERSION, + }, + account: { + ids: process.env.ANALYTICS_REPORT_IDS, + agency_name: process.env.AGENCY_NAME, + // needed for realtime reports which don't include hostname + // leave blank if your view includes hostnames + hostname: process.env.ANALYTICS_HOSTNAME || "", + }, + postgres: knexfile[process.env.NODE_ENV || "development"].connection, + static: { + path: "../analytics.usa.gov/", + }, +}; diff --git a/src/config.js b/src/config.js index 99f33495..5c897329 100644 --- a/src/config.js +++ b/src/config.js @@ -1,13 +1,15 @@ -const knexfile = require('../knexfile'); +const knexfile = require("../knexfile"); // Set environment variables to configure the application. module.exports = { email: process.env.ANALYTICS_REPORT_EMAIL, key: process.env.ANALYTICS_KEY, - key_file: process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.ANALYTICS_KEY_PATH, + key_file: + process.env.GOOGLE_APPLICATION_CREDENTIALS || + process.env.ANALYTICS_KEY_PATH, analytics_credentials: process.env.ANALYTICS_CREDENTIALS, reports_file: process.env.ANALYTICS_REPORTS_PATH, - debug: (process.env.ANALYTICS_DEBUG ? true : false), + debug: process.env.ANALYTICS_DEBUG ? true : false, /* AWS S3 information. @@ -24,7 +26,7 @@ module.exports = { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, s3ForcePathStyle: process.env.AWS_S3_FORCE_STYLE_PATH, - signatureVersion: process.env.AWS_SIGNATURE_VERSION + signatureVersion: process.env.AWS_SIGNATURE_VERSION, }, account: { ids: process.env.ANALYTICS_REPORT_IDS, @@ -35,6 +37,6 @@ module.exports = { }, postgres: knexfile[process.env.NODE_ENV || "development"].connection, static: { - path: '../analytics.usa.gov/', + path: "../analytics.usa.gov/", }, }; diff --git a/src/google-analytics/client.js b/src/google-analytics/client.js index c59db5b8..bc92940f 100644 --- a/src/google-analytics/client.js +++ b/src/google-analytics/client.js @@ -1,26 +1,20 @@ -const { BetaAnalyticsDataClient } = require("@google-analytics/data") +const { BetaAnalyticsDataClient } = require("@google-analytics/data"); const analyticsDataClient = new BetaAnalyticsDataClient(); -const GoogleAnalyticsQueryAuthorizer = require("./query-authorizer") -const GoogleAnalyticsQueryBuilder = require("./query-builder") +const GoogleAnalyticsQueryAuthorizer = require("./query-authorizer"); +const GoogleAnalyticsQueryBuilder = require("./query-builder"); const fetchData = (report) => { - const query = GoogleAnalyticsQueryBuilder.buildQuery(report) + const query = GoogleAnalyticsQueryBuilder.buildQuery(report); - return GoogleAnalyticsQueryAuthorizer.authorizeQuery(query).then(query => { - return _executeFetchDataRequest({ realtime: report.realtime }, query) - }) -} - -const _executeFetchDataRequest = ({ realtime }, query) => { - return new Promise(async (resolve, reject) => { - try { - const data = await _get(realtime, query); - resolve(data); - } catch (err) { - reject(err); - } + return GoogleAnalyticsQueryAuthorizer.authorizeQuery(query).then((query) => { + return _executeFetchDataRequest({ realtime: report.realtime }, query); }); -} +}; + +const _executeFetchDataRequest = async ({ realtime }, query) => { + const data = await _get(realtime, query); + return data; +}; async function _get(realtime, query) { if (realtime === true) { diff --git a/src/google-analytics/credential-loader.js b/src/google-analytics/credential-loader.js index 1879c4ed..daf918b7 100644 --- a/src/google-analytics/credential-loader.js +++ b/src/google-analytics/credential-loader.js @@ -1,20 +1,22 @@ -const config = require("../config") +const config = require("../config"); -global.analyticsCredentialsIndex = 0 +global.analyticsCredentialsIndex = 0; const loadCredentials = () => { - const credentialData = JSON.parse(config.analytics_credentials) - const credentialsArray = _wrapArray(credentialData) - const index = global.analyticsCredentialsIndex++ % credentialsArray.length - return credentialsArray[index] -} + const credentialData = JSON.parse( + Buffer.from(config.analytics_credentials, "base64").toString("utf8"), + ); + const credentialsArray = _wrapArray(credentialData); + const index = global.analyticsCredentialsIndex++ % credentialsArray.length; + return credentialsArray[index]; +}; const _wrapArray = (object) => { if (Array.isArray(object)) { - return object + return object; } else { - return [object] + return [object]; } -} +}; -module.exports = { loadCredentials } +module.exports = { loadCredentials }; diff --git a/src/google-analytics/query-authorizer.js b/src/google-analytics/query-authorizer.js index 30e1c3a3..4742df5e 100644 --- a/src/google-analytics/query-authorizer.js +++ b/src/google-analytics/query-authorizer.js @@ -1,55 +1,55 @@ -const googleapis = require('googleapis') -const fs = require('fs') -const config = require('../config') -const GoogleAnalyticsCredentialLoader = require("./credential-loader") +const googleapis = require("googleapis"); +const fs = require("fs"); +const config = require("../config"); +const GoogleAnalyticsCredentialLoader = require("./credential-loader"); const authorizeQuery = (query) => { - const credentials = _getCredentials() - const email = credentials.email - const key = credentials.key + const credentials = _getCredentials(); + const email = credentials.email; + const key = credentials.key; // https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/runReport#authorization-scopes - const scopes = ['https://www.googleapis.com/auth/analytics.readonly'] + const scopes = ["https://www.googleapis.com/auth/analytics.readonly"]; const jwt = new googleapis.Auth.JWT(email, null, key, scopes); - query = Object.assign({}, query, { auth: jwt }) + query = Object.assign({}, query, { auth: jwt }); return new Promise((resolve, reject) => { - jwt.authorize((err, result) => { + jwt.authorize((err) => { if (err) { - reject(err) + reject(err); } else { - resolve(query) + resolve(query); } - }) - }) -} + }); + }); +}; const _getCredentials = () => { if (config.key) { - return { key: config.key, email: config.email } + return { key: config.key, email: config.email }; } else if (config.key_file) { - return _loadCredentialsFromKeyfile(config.key_file) + return _loadCredentialsFromKeyfile(config.key_file); } else if (config.analytics_credentials) { - return GoogleAnalyticsCredentialLoader.loadCredentials() + return GoogleAnalyticsCredentialLoader.loadCredentials(); } else { - throw new Error("No key or key file specified in config") + throw new Error("No key or key file specified in config"); } -} +}; const _loadCredentialsFromKeyfile = (keyfile) => { if (!fs.existsSync(keyfile)) { - throw new Error(`No such key file: ${keyfile}`) + throw new Error(`No such key file: ${keyfile}`); } - let key = fs.readFileSync(keyfile).toString().trim() - let email = config.email + let key = fs.readFileSync(keyfile).toString().trim(); + let email = config.email; if (keyfile.match(/\.json$/)) { - const json = JSON.parse(key) - key = json.private_key - email = json.client_email + const json = JSON.parse(key); + key = json.private_key; + email = json.client_email; } - return { key, email } -} + return { key, email }; +}; -module.exports = { authorizeQuery } +module.exports = { authorizeQuery }; diff --git a/src/google-analytics/query-builder.js b/src/google-analytics/query-builder.js index a09ef8ad..b94b33d5 100644 --- a/src/google-analytics/query-builder.js +++ b/src/google-analytics/query-builder.js @@ -1,11 +1,11 @@ -const config = require('../config') +const config = require("../config"); const buildQuery = (report) => { - let query = Object.assign({}, report.query) - query.limit = query['limit'] || "10000" - query.property = `properties/${config.account.ids}` + let query = Object.assign({}, report.query); + query.limit = query["limit"] || "10000"; + query.property = `properties/${config.account.ids}`; query.ids = config.account.ids; - return query -} + return query; +}; -module.exports = { buildQuery } +module.exports = { buildQuery }; diff --git a/src/logger.js b/src/logger.js index 9c5cdbc3..cf2d5b7c 100644 --- a/src/logger.js +++ b/src/logger.js @@ -1,7 +1,7 @@ const winston = require("winston"); const logLevel = () => { - return process.env.ANALYTICS_LOG_LEVEL || "debug" + return process.env.ANALYTICS_LOG_LEVEL || "debug"; }; // Application logger configuration @@ -19,5 +19,8 @@ module.exports = { }), ], }); - } + }, + tag: (reportName) => { + return `[${reportName}: ${process.env.AGENCY_NAME || "gov-wide"}]`; + }, }; diff --git a/src/process-results/ga-data-processor.js b/src/process-results/ga-data-processor.js index 0e594692..3a51c003 100644 --- a/src/process-results/ga-data-processor.js +++ b/src/process-results/ga-data-processor.js @@ -1,5 +1,5 @@ -const config = require("../config") -const ResultTotalsCalculator = require("./result-totals-calculator") +const config = require("../config"); +const ResultTotalsCalculator = require("./result-totals-calculator"); /** * @param {Object} report The report object that was requested @@ -11,7 +11,7 @@ const ResultTotalsCalculator = require("./result-totals-calculator") * the original report and query. */ const processData = (report, data, query) => { - let result = _initializeResult({ report, data, query }) + let result = _initializeResult({ report, data, query }); // If you use a filter that results in no data, you get null // back from google and need to protect against it. @@ -21,164 +21,168 @@ const processData = (report, data, query) => { // Some reports may decide to cut fields from the output. if (report.cut) { - data = _removeColumnFromData({ column: report.cut, data }) + data = _removeColumnFromData({ column: report.cut, data }); } // Remove data points that are below the threshold if one exists if (report.threshold) { - data = _filterRowsBelowThreshold({ threshold: report.threshold, data }) + data = _filterRowsBelowThreshold({ threshold: report.threshold, data }); } // Process each row - result.data = data.rows.map(row => { - return _processRow({ row, report, data }) - }) + result.data = data.rows.map((row) => { + return _processRow({ row, report, data }); + }); - result.totals = ResultTotalsCalculator.calculateTotals(result) + result.totals = ResultTotalsCalculator.calculateTotals(result); return result; -} +}; const _fieldNameForColumnIndex = ({ entryKey, index, data }) => { // data keys come back as values for the header keys - const targetKey = entryKey.replace('Values', 'Headers') - const name = data[targetKey][index].name - return _mapping[name] || name -} + const targetKey = entryKey.replace("Values", "Headers"); + const name = data[targetKey][index].name; + return _mapping[name] || name; +}; const _filterRowsBelowThreshold = ({ threshold, data }) => { - data = Object.assign({}, data) + data = Object.assign({}, data); - const column = _findDimensionOrMetricIndex(threshold.field, data) + const column = _findDimensionOrMetricIndex(threshold.field, data); if (column != null) { - data.rows = data.rows.filter(row => { - return parseInt(row[column.rowKey][column.index].value) >= parseInt(threshold.value) - }) + data.rows = data.rows.filter((row) => { + return ( + parseInt(row[column.rowKey][column.index].value) >= + parseInt(threshold.value) + ); + }); } - return data -} + return data; +}; /** * 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. */ -_findDimensionOrMetricIndex = (name, data) => { - const dimensionIndex = data.dimensionHeaders.findIndex(header => { - return header.name === name - }) +const _findDimensionOrMetricIndex = (name, data) => { + const dimensionIndex = data.dimensionHeaders.findIndex((header) => { + return header.name === name; + }); if (dimensionIndex === -1) { - const metricIndex = data.metricHeaders.findIndex(header => { - return header.name === name - }) + const metricIndex = data.metricHeaders.findIndex((header) => { + return header.name === name; + }); if (metricIndex === -1) { return null; } else { - return { rowKey: 'metricValues', index: metricIndex }; + return { rowKey: "metricValues", index: metricIndex }; } } else { - return { rowKey: 'dimensionValues', index: dimensionIndex }; + return { rowKey: "dimensionValues", index: dimensionIndex }; } -} +}; const _formatDate = (date) => { if (date == "(other)") { - return date + return date; } - return [date.substr(0, 4), date.substr(4, 2), date.substr(6, 2)].join("-") -} + return [date.substr(0, 4), date.substr(4, 2), date.substr(6, 2)].join("-"); +}; const _initializeResult = ({ report, data, query }) => ({ name: report.name, sampling: data.metadata?.samplingMetadatas, query: ((query) => { - query = Object.assign({}, query) - delete query.ids - return query + query = Object.assign({}, query); + delete query.ids; + return query; })(query), meta: report.meta, data: [], totals: {}, taken_at: new Date(), -}) +}); -const _processRow = ({ row, data, report }) => { - const point = {} +const _processRow = ({ row, data }) => { + const point = {}; // Iterate through each entry in the object for (const [entryKey, entryValue] of Object.entries(row)) { - // Iterate through each object in the array entryValue.forEach((item, index) => { // Iterate through each key-value pair in the object for (const [key, value] of Object.entries(item)) { - if (key !== 'oneValue') { - const field = _fieldNameForColumnIndex({ entryKey, index, data }) + if (key !== "oneValue") { + const field = _fieldNameForColumnIndex({ entryKey, index, data }); let modValue; if (field === "date") { - modValue = _formatDate(value) + modValue = _formatDate(value); } else { - modValue = value + modValue = value; } - point[field] = modValue + point[field] = modValue; } } }); } - if (config.account.hostname && !('domain' in point)) { - point.domain = config.account.hostname + if (config.account.hostname && !("domain" in point)) { + point.domain = config.account.hostname; } - return point -} + return point; +}; const _removeColumnFromData = ({ column, data }) => { - data = Object.assign(data) + data = Object.assign(data); - const columnToRemove = _findDimensionOrMetricIndex(column, data) + const columnToRemove = _findDimensionOrMetricIndex(column, data); if (columnToRemove != null) { - data[columnToRemove.rowKey.replace('Values', 'Headers')].splice(columnToRemove.index, 1) - data.rows.forEach(row => { - row[columnToRemove.rowKey].splice(columnToRemove.index, 1) - }) + data[columnToRemove.rowKey.replace("Values", "Headers")].splice( + columnToRemove.index, + 1, + ); + data.rows.forEach((row) => { + row[columnToRemove.rowKey].splice(columnToRemove.index, 1); + }); } - return data -} + return data; +}; const _mapping = { - "activeUsers": "active_visitors", - "fileName": "file_name", - "fullPageUrl": "page", - "pageTitle": "page_title", - "unifiedScreenName": "page_title", - "sessions": "visits", - "deviceCategory": "device", - "operatingSystem": "os", - "operatingSystemVersion": "os_version", - "hostName": "domain", - "languageCode": "language_code", - "sessionSource": "source", - "screenPageViews": "visits", - "eventName": "event_label", - "eventCount": "total_events", - "landingPagePlusQueryString": "landing_page", - "sessionDefaultChannelGroup": "session_default_channel_group", - "screenPageViews": "pageviews", - "totalUsers": "users", - "screenPageViewsPerSession": "pageviews_per_session", - "averageSessionDuration": "avg_session_duration", - "bounceRate": "bounce_rate", - "screenResolution": "screen_resolution", - "mobileDeviceModel": "mobile_device", -} - -module.exports = { processData } + activeUsers: "active_visitors", + fileName: "file_name", + fullPageUrl: "page", + pageTitle: "page_title", + unifiedScreenName: "page_title", + sessions: "visits", + deviceCategory: "device", + operatingSystem: "os", + operatingSystemVersion: "os_version", + hostName: "domain", + languageCode: "language_code", + sessionSource: "source", + eventName: "event_label", + eventCount: "total_events", + landingPagePlusQueryString: "landing_page", + sessionDefaultChannelGroup: "session_default_channel_group", + screenPageViews: "pageviews", + totalUsers: "users", + screenPageViewsPerSession: "pageviews_per_session", + averageSessionDuration: "avg_session_duration", + bounceRate: "bounce_rate", + screenResolution: "screen_resolution", + mobileDeviceModel: "mobile_device", +}; + +module.exports = { processData }; diff --git a/src/process-results/result-formatter.js b/src/process-results/result-formatter.js index fd0f51bd..e711cb2e 100644 --- a/src/process-results/result-formatter.js +++ b/src/process-results/result-formatter.js @@ -1,38 +1,35 @@ -const csv = require("fast-csv") -const util = require('util'); -const logger = require('../logger').initialize(); - +const csv = require("fast-csv"); +const util = require("util"); +const logger = require("../logger").initialize(); const formatResult = (result, { format = "json", slim = false } = {}) => { - result = Object.assign({}, result) + result = Object.assign({}, result); switch (format) { case "json": - return _formatJSON(result, { slim }) - break + return _formatJSON(result, { slim }); case "csv": - return _formatCSV(result) - break + return _formatCSV(result); default: - return Promise.reject("Unsupported format: " + format) + return Promise.reject("Unsupported format: " + format); } -} +}; const _formatJSON = (result, { slim }) => { if (slim) { - delete result.data + delete result.data; } try { - return Promise.resolve(JSON.stringify(result, null, 2)) + return Promise.resolve(JSON.stringify(result, null, 2)); } catch (e) { - logger.error('Cannot stringify JSON'); + logger.error("Cannot stringify JSON"); logger.error(util.inspect(result)); - return Promise.reject(e) + return Promise.reject(e); } -} +}; const _formatCSV = (result) => { - return csv.writeToString(result.data, { headers: true }) -} + return csv.writeToString(result.data, { headers: true }); +}; -module.exports = { formatResult } +module.exports = { formatResult }; diff --git a/src/process-results/result-totals-calculator.js b/src/process-results/result-totals-calculator.js index 7db79ebe..63412f91 100644 --- a/src/process-results/result-totals-calculator.js +++ b/src/process-results/result-totals-calculator.js @@ -3,14 +3,14 @@ const calculateTotals = (result) => { return {}; } - let totals = {} + let totals = {}; // Sum up simple columns if ("users" in result.data[0]) { - totals.users = _sumColumn({ column: "users", result }) + totals.users = _sumColumn({ column: "users", result }); } if ("visits" in result.data[0]) { - totals.visits = _sumColumn({ column: "visits", result }) + totals.visits = _sumColumn({ column: "visits", result }); } // Sum up categories @@ -18,47 +18,47 @@ const calculateTotals = (result) => { totals.device_models = _sumVisitsByColumn({ column: "mobile_device", result, - }) + }); } if (result.name.match(/^language/)) { totals.languages = _sumVisitsByColumn({ column: "language", result, - }) + }); totals.language_codes = _sumVisitsByColumn({ column: "language_code", result, - }) + }); } if (result.name.match(/^devices/)) { totals.devices = _sumVisitsByColumn({ column: "device", result, - }) + }); } if (result.name == "screen-size") { totals.screen_resolution = _sumVisitsByColumn({ column: "screen_resolution", result, - }) + }); } if (result.name === "os" || result.name === "os-90-days") { totals.os = _sumVisitsByColumn({ column: "os", result, - }) + }); } if (result.name === "windows" || result.name === "windows-90-days") { totals.os_version = _sumVisitsByColumn({ column: "os_version", result, - }) + }); } if (result.name === "browsers" || result.name === "browsers-90-days") { totals.browser = _sumVisitsByColumn({ column: "browser", result, - }) + }); } // Sum up totals with 2 levels of hashes @@ -67,68 +67,68 @@ const calculateTotals = (result) => { column: "os", dimension: "browser", result, - }) + }); totals.by_browsers = _sumVisitsByCategoryWithDimension({ column: "browser", dimension: "os", result, - }) + }); } if (result.name === "windows-browsers") { totals.by_windows = _sumVisitsByCategoryWithDimension({ column: "os_version", dimension: "browser", result, - }) + }); totals.by_browsers = _sumVisitsByCategoryWithDimension({ column: "browser", dimension: "os_version", result, - }) + }); } // Set the start and end date if (result.data[0].data) { // Occasionally we'll get bogus start dates if (result.date[0].date === "(other)") { - totals.start_date = result.data[1].date + totals.start_date = result.data[1].date; } else { - totals.start_date = result.data[0].date + totals.start_date = result.data[0].date; } - totals.end_date = result.data[result.data.length - 1].date + totals.end_date = result.data[result.data.length - 1].date; } - return totals -} + return totals; +}; const _sumColumn = ({ result, column }) => { return result.data.reduce((total, row) => { - return parseInt(row[column]) + total - }, 0) -} + return parseInt(row[column]) + total; + }, 0); +}; const _sumVisitsByColumn = ({ result, column }) => { return result.data.reduce((categories, row) => { - const category = row[column] - const visits = parseInt(row.visits) - categories[category] = (categories[category] || 0) + visits - return categories - }, {}) -} + const category = row[column]; + const visits = parseInt(row.visits); + categories[category] = (categories[category] || 0) + visits; + return categories; + }, {}); +}; const _sumVisitsByCategoryWithDimension = ({ result, column, dimension }) => { return result.data.reduce((categories, row) => { - const parentCategory = row[column] - const childCategory = row[dimension] - const visits = parseInt(row.visits) + const parentCategory = row[column]; + const childCategory = row[dimension]; + const visits = parseInt(row.visits); - categories[parentCategory] = categories[parentCategory] || {} + categories[parentCategory] = categories[parentCategory] || {}; - const newTotal = (categories[parentCategory][childCategory] || 0) + visits - categories[parentCategory][childCategory] = newTotal + const newTotal = (categories[parentCategory][childCategory] || 0) + visits; + categories[parentCategory][childCategory] = newTotal; - return categories - }, {}) -} + return categories; + }, {}); +}; -module.exports = { calculateTotals } +module.exports = { calculateTotals }; diff --git a/src/publish/disk.js b/src/publish/disk.js index 653f42ad..8c49377c 100644 --- a/src/publish/disk.js +++ b/src/publish/disk.js @@ -1,19 +1,10 @@ -const fs = require("fs") -const path = require("path") +const fs = require("node:fs/promises"); +const path = require("path"); -const publish = (report, results, { output, format }) => { - const filename = `${report.name}.${format}` - const filepath = path.join(output, filename) +const publish = async (report, results, { output, format }) => { + const filename = `${report.name}.${format}`; + const filepath = path.join(output, filename); + await fs.writeFile(filepath, results); +}; - return new Promise((resolve, reject) => { - fs.writeFile(filepath, results, err => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) -} - -module.exports = { publish } +module.exports = { publish }; diff --git a/src/publish/postgres.js b/src/publish/postgres.js index 647ae3ef..5d78943d 100644 --- a/src/publish/postgres.js +++ b/src/publish/postgres.js @@ -1,115 +1,120 @@ -const ANALYTICS_DATA_TABLE_NAME = "analytics_data_ga4" +const ANALYTICS_DATA_TABLE_NAME = "analytics_data_ga4"; -const knex = require("knex") -const config = require("../config") +const knex = require("knex"); +const config = require("../config"); Promise.each = async function (arr, fn) { for (const item of arr) await fn(item); -} +}; const publish = (results) => { - if (results.query.dimensions.some(obj => obj.name === 'date')) { - const db = knex({ client: "pg", connection: config.postgres }) - return _writeRegularResults({ db, results }).then(() => db.destroy()) + if (results.query.dimensions.some((obj) => obj.name === "date")) { + const db = knex({ client: "pg", connection: config.postgres }); + return _writeRegularResults({ db, results }).then(() => db.destroy()); } else { - return Promise.resolve() + return Promise.resolve(); } -} +}; const _convertDataAttributesToNumbers = (data) => { - const transformedData = Object.assign({}, data) + const transformedData = Object.assign({}, data); - const numbericalAttributes = ["visits", "total_events", "users"] - numbericalAttributes.forEach(attributeName => { + const numbericalAttributes = ["visits", "total_events", "users"]; + numbericalAttributes.forEach((attributeName) => { if (transformedData[attributeName]) { - transformedData[attributeName] = Number(transformedData[attributeName]) + transformedData[attributeName] = Number(transformedData[attributeName]); } - }) + }); - return transformedData -} + return transformedData; +}; const _dataForDataPoint = (dataPoint) => { - const data = _convertDataAttributesToNumbers(dataPoint) + const data = _convertDataAttributesToNumbers(dataPoint); - const date = _dateTimeForDataPoint(dataPoint) + const date = _dateTimeForDataPoint(dataPoint); - delete data.date - delete data.hour + delete data.date; + delete data.hour; return { date, data, - } -} + }; +}; const _dateTimeForDataPoint = (dataPoint) => { if (!isNaN(Date.parse(dataPoint.date))) { - return dataPoint.date + return dataPoint.date; } -} +}; const _queryForExistingRow = ({ db, row }) => { - query = db(ANALYTICS_DATA_TABLE_NAME) + let query = db(ANALYTICS_DATA_TABLE_NAME); - Object.keys(row).forEach(key => { + Object.keys(row).forEach((key) => { if (row[key] === undefined) { - return + return; } else if (key === "data") { - const dataQuery = Object.assign({}, row.data) - delete dataQuery.visits - delete dataQuery.users - delete dataQuery.total_events - Object.keys(dataQuery).forEach(dataKey => { - query = query.whereRaw(`data->>'${dataKey}' = ?`, [dataQuery[dataKey]]) - }) + const dataQuery = Object.assign({}, row.data); + delete dataQuery.visits; + delete dataQuery.users; + delete dataQuery.total_events; + Object.keys(dataQuery).forEach((dataKey) => { + query = query.whereRaw(`data->>'${dataKey}' = ?`, [dataQuery[dataKey]]); + }); } else { - query = query.where({ [key]: row[key] }) + query = query.where({ [key]: row[key] }); } - }) + }); - return query.select() -} + return query.select(); +}; const _handleExistingRow = ({ db, existingRow, newRow }) => { - if (existingRow.data.visits != newRow.data.visits || + if ( + existingRow.data.visits != newRow.data.visits || existingRow.data.users != newRow.data.users || existingRow.data.total_events != newRow.data.total_events ) { - return db(ANALYTICS_DATA_TABLE_NAME).where({ id: existingRow.id }).update(newRow) + return db(ANALYTICS_DATA_TABLE_NAME) + .where({ id: existingRow.id }) + .update(newRow); } -} +}; const _rowForDataPoint = ({ results, dataPoint }) => { - const row = _dataForDataPoint(dataPoint) - row.report_name = results.name - row.report_agency = results.agency - return row -} + const row = _dataForDataPoint(dataPoint); + row.report_name = results.name; + row.report_agency = results.agency; + return row; +}; const _writeRegularResults = ({ db, results }) => { - const rows = results.data.map(dataPoint => { - return _rowForDataPoint({ results, dataPoint }) - }) + const rows = results.data.map((dataPoint) => { + return _rowForDataPoint({ results, dataPoint }); + }); - const rowsToInsert = [] - return Promise.each(rows, row => { - return _queryForExistingRow({ db, row }).then(results => { + const rowsToInsert = []; + return Promise.each(rows, (row) => { + return _queryForExistingRow({ db, row }).then((results) => { if (row.date === undefined) { - return + return; } else if (results.length === 0) { - rowsToInsert.push(row) + rowsToInsert.push(row); } else if (results.length === 1) { - return _handleExistingRow({ db, existingRow: results[0], newRow: row }) + return _handleExistingRow({ db, existingRow: results[0], newRow: row }); } - }) - }).then(() => { - if (rowsToInsert.length > 0) { - return db(ANALYTICS_DATA_TABLE_NAME).insert(rowsToInsert) - } - }).then(() => { - return db.destroy() + }); }) -} + .then(() => { + if (rowsToInsert.length > 0) { + return db(ANALYTICS_DATA_TABLE_NAME).insert(rowsToInsert); + } + }) + .then(() => { + return db.destroy(); + }); +}; -module.exports = { publish, ANALYTICS_DATA_TABLE_NAME } +module.exports = { publish, ANALYTICS_DATA_TABLE_NAME }; diff --git a/src/publish/s3.js b/src/publish/s3.js index 9fd9159b..f4628968 100644 --- a/src/publish/s3.js +++ b/src/publish/s3.js @@ -1,13 +1,15 @@ const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); -const zlib = require("zlib") -const config = require("../config") -const logger = require('../../src/logger').initialize(); +const zlib = require("zlib"); +const config = require("../config"); +const Logger = require("../../src/logger"); +const logger = Logger.initialize(); // This is the case where using custom s3 api-like services like minio. const s3config = { accessKeyId: config.aws.accessKeyId, secretAccessKey: config.aws.secretAccessKey, endpoint: config.aws.endpoint, + region: config.aws.region, s3ForcePathStyle: config.aws.s3ForcePathStyle, signatureVersion: config.aws.signatureVersion, }; @@ -15,7 +17,7 @@ const s3config = { const s3Client = new S3Client(s3config); const publish = async (report, results, { format }) => { logger.debug( - "[" + report.name + "] Publishing to " + config.aws.bucket + "..." + `${Logger.tag(report.name)} Publishing to ${config.aws.bucket}...`, ); const compressed = await _compress(results); @@ -27,7 +29,7 @@ const publish = async (report, results, { format }) => { ContentEncoding: "gzip", ACL: "public-read", CacheControl: "max-age=" + (config.aws.cache || 0), - }) + }); return s3Client.send(command); }; diff --git a/test/analytics.test.js b/test/analytics.test.js index 82cc356c..31594a24 100644 --- a/test/analytics.test.js +++ b/test/analytics.test.js @@ -1,61 +1,67 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); describe("Analytics", () => { - let Analytics - let GoogleAnalyticsClient - let GoogleAnalyticsDataProcessor + let Analytics; + let GoogleAnalyticsClient; + let GoogleAnalyticsDataProcessor; beforeEach(() => { - GoogleAnalyticsClient = {} - GoogleAnalyticsDataProcessor = {} + GoogleAnalyticsClient = {}; + GoogleAnalyticsDataProcessor = {}; Analytics = proxyquire("../src/analytics", { "./google-analytics/client": GoogleAnalyticsClient, "./process-results/ga-data-processor": GoogleAnalyticsDataProcessor, - }) - }) + }); + }); describe(".query(report)", () => { - it("should resolve with formatted google analytics data for the given reports", done => { - const report = { name: "Report Name" } - const data = [{ rows: [1, 2, 3] }] - const processedData = { data: [1, 2, 3] } + it("should resolve with formatted google analytics data for the given reports", (done) => { + const report = { name: "Report Name" }; + const data = [{ rows: [1, 2, 3] }]; + const processedData = { data: [1, 2, 3] }; - let fetchDataCalled = false - let processedDataCalled = false + let fetchDataCalled = false; + let processedDataCalled = false; GoogleAnalyticsClient.fetchData = (reportInput) => { - fetchDataCalled = true - expect(reportInput).to.equal(report) - return Promise.resolve(data) - } + fetchDataCalled = true; + expect(reportInput).to.equal(report); + return Promise.resolve(data); + }; GoogleAnalyticsDataProcessor.processData = (reportInput, dataInput) => { - processedDataCalled = true - expect(reportInput).to.equal(report) - expect(dataInput).to.equal(data[0]) - return Promise.resolve(processedData) - } - - Analytics.query(report).then(results => { - expect(results).to.equal(processedData) - expect(fetchDataCalled).to.be.true - expect(processedDataCalled).to.be.true - done() - }).catch(done) - }) - - it("should reject if no report is provided", done => { - Analytics.query().catch(err => { - expect(err.message).to.equal("Analytics.query missing required argument `report`") - done() - }).catch(done) - }) - }) + processedDataCalled = true; + expect(reportInput).to.equal(report); + expect(dataInput).to.equal(data[0]); + return Promise.resolve(processedData); + }; + + Analytics.query(report) + .then((results) => { + expect(results).to.equal(processedData); + expect(fetchDataCalled).to.be.true; + expect(processedDataCalled).to.be.true; + done(); + }) + .catch(done); + }); + + it("should reject if no report is provided", (done) => { + Analytics.query() + .catch((err) => { + expect(err.message).to.equal( + "Analytics.query missing required argument `report`", + ); + done(); + }) + .catch(done); + }); + }); describe(".reports", () => { it("should load reports", () => { - expect(Analytics.reports).to.be.an("array") - }) - }) -}) + expect(Analytics.reports).to.be.an("array"); + }); + }); +}); diff --git a/test/google-analytics/credential-loader.test.js b/test/google-analytics/credential-loader.test.js index 2cb92c02..3f61a35d 100644 --- a/test/google-analytics/credential-loader.test.js +++ b/test/google-analytics/credential-loader.test.js @@ -1,34 +1,41 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); -proxyquire.noCallThru() +proxyquire.noCallThru(); -const config = {} +const config = {}; -const GoogleAnalyticsCredentialLoader = proxyquire("../../src/google-analytics/credential-loader", { - "../config": config, -}) +const GoogleAnalyticsCredentialLoader = proxyquire( + "../../src/google-analytics/credential-loader", + { + "../config": config, + }, +); describe("GoogleAnalyticsCredentialLoader", () => { describe(".loadCredentials()", () => { beforeEach(() => { - config.analytics_credentials = undefined - global.analyticsCredentialsIndex = 0 - }) + config.analytics_credentials = undefined; + global.analyticsCredentialsIndex = 0; + }); it("should return the credentials if the credentials are an object", () => { - config.analytics_credentials = `{ + config.analytics_credentials = Buffer.from( + `{ "email": "email@example.com", "key": "this-is-a-secret" - }` + }`, + "utf8", + ).toString("base64"); - const creds = GoogleAnalyticsCredentialLoader.loadCredentials() - expect(creds.email).to.equal("email@example.com") - expect(creds.key).to.equal("this-is-a-secret") - }) + const creds = GoogleAnalyticsCredentialLoader.loadCredentials(); + expect(creds.email).to.equal("email@example.com"); + expect(creds.key).to.equal("this-is-a-secret"); + }); it("should return successive credentials if the credentials are an array", () => { - config.analytics_credentials = `[ + config.analytics_credentials = Buffer.from( + `[ { "email": "email_1@example.com", "key": "this-is-a-secret-1" @@ -37,18 +44,20 @@ describe("GoogleAnalyticsCredentialLoader", () => { "email": "email_2@example.com", "key": "this-is-a-secret-2" } - ]` - - const firstCreds = GoogleAnalyticsCredentialLoader.loadCredentials() - const secondCreds = GoogleAnalyticsCredentialLoader.loadCredentials() - const thirdCreds = GoogleAnalyticsCredentialLoader.loadCredentials() - - expect(firstCreds.email).to.equal("email_1@example.com") - expect(firstCreds.key).to.equal("this-is-a-secret-1") - expect(secondCreds.email).to.equal("email_2@example.com") - expect(secondCreds.key).to.equal("this-is-a-secret-2") - expect(thirdCreds.email).to.equal("email_1@example.com") - expect(thirdCreds.key).to.equal("this-is-a-secret-1") - }) - }) -}) + ]`, + "utf8", + ).toString("base64"); + + const firstCreds = GoogleAnalyticsCredentialLoader.loadCredentials(); + const secondCreds = GoogleAnalyticsCredentialLoader.loadCredentials(); + const thirdCreds = GoogleAnalyticsCredentialLoader.loadCredentials(); + + expect(firstCreds.email).to.equal("email_1@example.com"); + expect(firstCreds.key).to.equal("this-is-a-secret-1"); + expect(secondCreds.email).to.equal("email_2@example.com"); + expect(secondCreds.key).to.equal("this-is-a-secret-2"); + expect(thirdCreds.email).to.equal("email_1@example.com"); + expect(thirdCreds.key).to.equal("this-is-a-secret-1"); + }); + }); +}); diff --git a/test/google-analytics/query-authorizer.test.js b/test/google-analytics/query-authorizer.test.js index 626978d9..d61584e8 100644 --- a/test/google-analytics/query-authorizer.test.js +++ b/test/google-analytics/query-authorizer.test.js @@ -1,125 +1,154 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") -const googleAPIsMock = require("../support/mocks/googleapis-auth") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const googleAPIsMock = require("../support/mocks/googleapis-auth"); -proxyquire.noCallThru() +proxyquire.noCallThru(); -const config = {} -const googleapis = {} +const config = {}; +const googleapis = {}; const GoogleAnalyticsCredentialLoader = { loadCredentials: () => ({ email: "next_email@example.com", key: "Shhh, this is the next secret", - }) -} - -const GoogleAnalyticsQueryAuthorizer = proxyquire("../../src/google-analytics/query-authorizer", { - "../config": config, - "./credential-loader": GoogleAnalyticsCredentialLoader, - googleapis, -}) + }), +}; + +const GoogleAnalyticsQueryAuthorizer = proxyquire( + "../../src/google-analytics/query-authorizer", + { + "../config": config, + "./credential-loader": GoogleAnalyticsCredentialLoader, + googleapis, + }, +); describe("GoogleAnalyticsQueryAuthorizer", () => { describe(".authorizeQuery(query)", () => { beforeEach(() => { - Object.assign(googleapis, googleAPIsMock()) - config.email = "hello@example.com" - config.key = "123abc" - config.key_file = undefined - }) - - it("should resolve a query with the auth prop set to an authorized JWT", done => { - query = { - "abc": 123 - } - - GoogleAnalyticsQueryAuthorizer.authorizeQuery(query).then(query => { - expect(query.abc).to.equal(123) - expect(query.auth).to.not.be.undefined - expect(query.auth).to.be.an.instanceof(googleapis.Auth.JWT) - done() - }).catch(done) - }) - - it("should create a JWT with the key and email in the config if one exists", done => { - config.email = "test@example.com" - config.key = "Shh, this is a secret" - - GoogleAnalyticsQueryAuthorizer.authorizeQuery({}).then(query => { - expect(query.auth.initArguments[0]).to.equal("test@example.com") - expect(query.auth.initArguments[2]).to.equal("Shh, this is a secret") - done() - }).catch(done) - }) - - it("should create a JWT from the keyfile and the email in the config if one exists", done => { - config.email = "test@example.com" - config.key = undefined - config.key_file = "./test/support/fixtures/secret_key.pem" - - GoogleAnalyticsQueryAuthorizer.authorizeQuery({}).then(query => { - expect(query.auth.initArguments[0]).to.equal("test@example.com") - expect(query.auth.initArguments[2]).to.equal("pem-key-file-not-actually-a-secret-key") - done() - }).catch(done) - }) - - it("should create a JWT from the JSON keyfile in the config if one exists", done => { - config.key = undefined - config.key_file = "./test/support/fixtures/secret_key.json" - - GoogleAnalyticsQueryAuthorizer.authorizeQuery({}).then(query => { - expect(query.auth.initArguments[0]).to.equal("json_test_email@example.com") - expect(query.auth.initArguments[2]).to.equal("json-key-file-not-actually-a-secret-key") - done() - }).catch(done) - }) - - it("should create a JWT with credentials from calling GoogleAnalyticsCredentialLoader for analytics credentials in the config", done => { - config.key = undefined - config.analytics_credentials = "[{}]" // overriden by proxyquire - - GoogleAnalyticsQueryAuthorizer.authorizeQuery({}).then(query => { - expect(query.auth.initArguments[0]).to.equal("next_email@example.com") - expect(query.auth.initArguments[2]).to.equal("Shhh, this is the next secret") - done() - }).catch(done) - }) - - it("should create a JWT with the proper scopes", done => { - GoogleAnalyticsQueryAuthorizer.authorizeQuery({}).then(query => { - expect(query.auth.initArguments[3]).to.deep.equal([ - "https://www.googleapis.com/auth/analytics.readonly" - ]) - done() - }).catch(done) - }) - - it("should authorize the JWT and resolve if it is valid", done => { - let jwtAuthorized = false + Object.assign(googleapis, googleAPIsMock()); + config.email = "hello@example.com"; + config.key = "123abc"; + config.key_file = undefined; + }); + + it("should resolve a query with the auth prop set to an authorized JWT", (done) => { + let query = { + abc: 123, + }; + + GoogleAnalyticsQueryAuthorizer.authorizeQuery(query) + .then((query) => { + expect(query.abc).to.equal(123); + expect(query.auth).to.not.be.undefined; + expect(query.auth).to.be.an.instanceof(googleapis.Auth.JWT); + done(); + }) + .catch(done); + }); + + it("should create a JWT with the key and email in the config if one exists", (done) => { + config.email = "test@example.com"; + config.key = "Shh, this is a secret"; + + GoogleAnalyticsQueryAuthorizer.authorizeQuery({}) + .then((query) => { + expect(query.auth.initArguments[0]).to.equal("test@example.com"); + expect(query.auth.initArguments[2]).to.equal("Shh, this is a secret"); + done(); + }) + .catch(done); + }); + + it("should create a JWT from the keyfile and the email in the config if one exists", (done) => { + config.email = "test@example.com"; + config.key = undefined; + config.key_file = "./test/support/fixtures/secret_key.pem"; + + GoogleAnalyticsQueryAuthorizer.authorizeQuery({}) + .then((query) => { + expect(query.auth.initArguments[0]).to.equal("test@example.com"); + expect(query.auth.initArguments[2]).to.equal( + "pem-key-file-not-actually-a-secret-key", + ); + done(); + }) + .catch(done); + }); + + it("should create a JWT from the JSON keyfile in the config if one exists", (done) => { + config.key = undefined; + config.key_file = "./test/support/fixtures/secret_key.json"; + + GoogleAnalyticsQueryAuthorizer.authorizeQuery({}) + .then((query) => { + expect(query.auth.initArguments[0]).to.equal( + "json_test_email@example.com", + ); + expect(query.auth.initArguments[2]).to.equal( + "json-key-file-not-actually-a-secret-key", + ); + done(); + }) + .catch(done); + }); + + it("should create a JWT with credentials from calling GoogleAnalyticsCredentialLoader for analytics credentials in the config", (done) => { + config.key = undefined; + config.analytics_credentials = "[{}]"; // overriden by proxyquire + + GoogleAnalyticsQueryAuthorizer.authorizeQuery({}) + .then((query) => { + expect(query.auth.initArguments[0]).to.equal( + "next_email@example.com", + ); + expect(query.auth.initArguments[2]).to.equal( + "Shhh, this is the next secret", + ); + done(); + }) + .catch(done); + }); + + it("should create a JWT with the proper scopes", (done) => { + GoogleAnalyticsQueryAuthorizer.authorizeQuery({}) + .then((query) => { + expect(query.auth.initArguments[3]).to.deep.equal([ + "https://www.googleapis.com/auth/analytics.readonly", + ]); + done(); + }) + .catch(done); + }); + + it("should authorize the JWT and resolve if it is valid", (done) => { + let jwtAuthorized = false; googleapis.Auth.JWT.prototype.authorize = (callback) => { - jwtAuthorized = true - callback(null, {}) - } - - GoogleAnalyticsQueryAuthorizer.authorizeQuery({}).then(query => { - expect(jwtAuthorized).to.equal(true) - done() - }).catch(done) - }) - - it("should authorize the JWT and reject if it is invalid", done => { - let jwtAuthorized = false + jwtAuthorized = true; + callback(null, {}); + }; + + GoogleAnalyticsQueryAuthorizer.authorizeQuery({}) + .then(() => { + expect(jwtAuthorized).to.equal(true); + done(); + }) + .catch(done); + }); + + it("should authorize the JWT and reject if it is invalid", (done) => { + let jwtAuthorized = false; googleapis.Auth.JWT.prototype.authorize = (callback) => { - jwtAuthorized = true - callback(new Error("Failed to authorize")) - } - - GoogleAnalyticsQueryAuthorizer.authorizeQuery({}).catch(err => { - expect(jwtAuthorized).to.equal(true) - expect(err.message).to.equal("Failed to authorize") - done() - }).catch(done) - }) - }) -}) + jwtAuthorized = true; + callback(new Error("Failed to authorize")); + }; + + GoogleAnalyticsQueryAuthorizer.authorizeQuery({}) + .catch((err) => { + expect(jwtAuthorized).to.equal(true); + expect(err.message).to.equal("Failed to authorize"); + done(); + }) + .catch(done); + }); + }); +}); diff --git a/test/google-analytics/query-builder.test.js b/test/google-analytics/query-builder.test.js index 9dce2d33..0b27d64d 100644 --- a/test/google-analytics/query-builder.test.js +++ b/test/google-analytics/query-builder.test.js @@ -1,56 +1,59 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") -const reportFixture = require("../support/fixtures/report") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const reportFixture = require("../support/fixtures/report"); -proxyquire.noCallThru() +proxyquire.noCallThru(); -const config = {} +const config = {}; -const GoogleAnalyticsQueryBuilder = proxyquire("../../src/google-analytics/query-builder", { - "../config": config, -}) +const GoogleAnalyticsQueryBuilder = proxyquire( + "../../src/google-analytics/query-builder", + { + "../config": config, + }, +); describe("GoogleAnalyticsQueryBuilder", () => { describe(".buildQuery(report)", () => { - let report + let report; beforeEach(() => { - report = Object.assign({}, reportFixture) + report = Object.assign({}, reportFixture); config.account = { ids: "ga:123456", - } - }) + }; + }); it("should set the properties from the query object on the report", () => { report.query = { a: "123abc", b: "456def", - } + }; - const query = GoogleAnalyticsQueryBuilder.buildQuery(report) - expect(query.a).to.equal("123abc") - expect(query.b).to.equal("456def") - }) + const query = GoogleAnalyticsQueryBuilder.buildQuery(report); + expect(query.a).to.equal("123abc"); + expect(query.b).to.equal("456def"); + }); it("should set limit if it is set on the report", () => { - report.query["limit"] = "3" + report.query["limit"] = "3"; - const query = GoogleAnalyticsQueryBuilder.buildQuery(report) - expect(query["limit"]).to.equal("3") - }) + const query = GoogleAnalyticsQueryBuilder.buildQuery(report); + expect(query["limit"]).to.equal("3"); + }); it("should set limit to 10000 if it is unset on the report", () => { - report.query["limit"] = undefined + report.query["limit"] = undefined; - const query = GoogleAnalyticsQueryBuilder.buildQuery(report) - expect(query["limit"]).to.equal("10000") - }) + const query = GoogleAnalyticsQueryBuilder.buildQuery(report); + expect(query["limit"]).to.equal("10000"); + }); it("should set the ids to the account ids specified by the config", () => { - config.account.ids = "ga:abc123" + config.account.ids = "ga:abc123"; - const query = GoogleAnalyticsQueryBuilder.buildQuery(report) - expect(query.ids).to.equal("ga:abc123") - }) - }) -}) + const query = GoogleAnalyticsQueryBuilder.buildQuery(report); + expect(query.ids).to.equal("ga:abc123"); + }); + }); +}); diff --git a/test/index.test.js b/test/index.test.js index d5af8368..830403fd 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,36 +1,36 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") -const resultFixture = require("./support/fixtures/results") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); describe("main", () => { describe(".run(options)", () => { - const consoleLogOriginal = console.log + const consoleLogOriginal = console.log; after(() => { - console.log = consoleLogOriginal - }) + console.log = consoleLogOriginal; + }); - const config = {} + const config = {}; - let Analytics - let DiskPublisher - let PostgresPublisher - let ResultFormatter - let S3Publisher - let result - let main + let Analytics; + let DiskPublisher; + let PostgresPublisher; + let ResultFormatter; + let S3Publisher; + let result; + let main; beforeEach(() => { - result = {} + result = {}; Analytics = { reports: [{ name: "a" }, { name: "b" }, { name: "c" }], - query: (report) => Promise.resolve(Object.assign(result, { name: report.name })), - } - DiskPublisher = {} - PostgresPublisher = {} + query: (report) => + Promise.resolve(Object.assign(result, { name: report.name })), + }; + DiskPublisher = {}; + PostgresPublisher = {}; ResultFormatter = { - formatResult: (result) => Promise.resolve(JSON.stringify(result)) - } - S3Publisher = {} + formatResult: (result) => Promise.resolve(JSON.stringify(result)), + }; + S3Publisher = {}; main = proxyquire("../index.js", { "./src/config": config, @@ -39,223 +39,259 @@ describe("main", () => { "./src/publish/postgres": PostgresPublisher, "./src/process-results/result-formatter": ResultFormatter, "./src/publish/s3": S3Publisher, - }) - }) + }); + }); - it("should query for every single report", done => { - const queriedReportNames = [] + it("should query for every single report", (done) => { + const queriedReportNames = []; Analytics.query = (report) => { - queriedReportNames.push(report.name) - return Promise.resolve(result) - } - - main.run().then(() => { - expect(queriedReportNames).to.include.members(["a", "b", "c"]) - done() - }).catch(done) - }) - - it("should log formatted results", done => { - ResultFormatter.formatResult = () => Promise.resolve("I'm the results!") - - let consoleLogCalled = false - console.log = function(output) { + queriedReportNames.push(report.name); + return Promise.resolve(result); + }; + + main + .run() + .then(() => { + expect(queriedReportNames).to.include.members(["a", "b", "c"]); + done(); + }) + .catch(done); + }); + + it("should log formatted results", (done) => { + ResultFormatter.formatResult = () => Promise.resolve("I'm the results!"); + + let consoleLogCalled = false; + console.log = function (output) { if (output === "I'm the results!") { - consoleLogCalled = true + consoleLogCalled = true; } else { - consoleLogOriginal.apply(this, arguments) + consoleLogOriginal.apply(this, arguments); } - } - - main.run().then(() => { - console.log = consoleLogOriginal - expect(consoleLogCalled).to.be.true - done() - }).catch(err => { - console.log = consoleLogOriginal - done(err) - }) - }) - - it("should format the results with the format set to JSON", done => { - let formatResultCalled = false + }; + + main + .run() + .then(() => { + console.log = consoleLogOriginal; + expect(consoleLogCalled).to.be.true; + done(); + }) + .catch((err) => { + console.log = consoleLogOriginal; + done(err); + }); + }); + + it("should format the results with the format set to JSON", (done) => { + let formatResultCalled = false; ResultFormatter.formatResult = (result, options) => { - expect(options.format).to.equal("json") - formatResultCalled = true - return Promise.resolve("") - } - - main.run().then(() => { - expect(formatResultCalled).to.be.true - done() - }).catch(done) - }) + expect(options.format).to.equal("json"); + formatResultCalled = true; + return Promise.resolve(""); + }; + + main + .run() + .then(() => { + expect(formatResultCalled).to.be.true; + done(); + }) + .catch(done); + }); context("with --output option", () => { - it("should write the results to the given path folder", done => { - ResultFormatter.formatResult = () => Promise.resolve("I'm the result") + it("should write the results to the given path folder", (done) => { + ResultFormatter.formatResult = () => Promise.resolve("I'm the result"); - const writtenReportNames = [] + const writtenReportNames = []; DiskPublisher.publish = (report, formattedResult, options) => { - expect(options.format).to.equal("json") - expect(options.output).to.equal("path/to/output") - expect(formattedResult).to.equal("I'm the result") - writtenReportNames.push(report.name) - } - - main.run({ output: "path/to/output" }).then(() => { - expect(writtenReportNames).to.include.members(["a", "b", "c"]) - done() - }).catch(done) - }) - }) + expect(options.format).to.equal("json"); + expect(options.output).to.equal("path/to/output"); + expect(formattedResult).to.equal("I'm the result"); + writtenReportNames.push(report.name); + }; + + main + .run({ output: "path/to/output" }) + .then(() => { + expect(writtenReportNames).to.include.members(["a", "b", "c"]); + done(); + }) + .catch(done); + }); + }); context("with --publish option", () => { - it("should publish the results to s3", done => { - result = { data: "I'm the result" } + it("should publish the results to s3", (done) => { + result = { data: "I'm the result" }; - const publishedReportNames = [] + const publishedReportNames = []; S3Publisher.publish = (report, formattedResult, options) => { - expect(options.format).to.equal("json") - expect(JSON.parse(formattedResult)).to.deep.equal(result) - publishedReportNames.push(report.name) - } - - main.run({ publish: true }).then(() => { - expect(publishedReportNames).to.include.members(["a", "b", "c"]) - done() - }).catch(done) - }) - }) + expect(options.format).to.equal("json"); + expect(JSON.parse(formattedResult)).to.deep.equal(result); + publishedReportNames.push(report.name); + }; + + main + .run({ publish: true }) + .then(() => { + expect(publishedReportNames).to.include.members(["a", "b", "c"]); + done(); + }) + .catch(done); + }); + }); context("with --write-to-database option", () => { - it("should write the results to postgres", done => { - result = { data: "I am the result" } + it("should write the results to postgres", (done) => { + result = { data: "I am the result" }; - let publishCalled = false + let publishCalled = false; PostgresPublisher.publish = (resultToPublish) => { - expect(resultToPublish).to.deep.equal(result) - publishCalled = true - return Promise.resolve() - } - - main.run({ ["write-to-database"]: true }).then(() => { - expect(publishCalled).to.be.true - done() - }).catch(done) - }) - - it("should not write the results to postgres if the report is realtime", done => { - let publishCalled = false + expect(resultToPublish).to.deep.equal(result); + publishCalled = true; + return Promise.resolve(); + }; + + main + .run({ ["write-to-database"]: true }) + .then(() => { + expect(publishCalled).to.be.true; + done(); + }) + .catch(done); + }); + + it("should not write the results to postgres if the report is realtime", (done) => { + let publishCalled = false; PostgresPublisher.publish = () => { - publishCalled = true - return Promise.resolve() - } - - main.run({ ["write-to-database"]: false }).then(() => { - expect(publishCalled).to.be.false - done() - }).catch(done) - }) - }) + publishCalled = true; + return Promise.resolve(); + }; + + main + .run({ ["write-to-database"]: false }) + .then(() => { + expect(publishCalled).to.be.false; + done(); + }) + .catch(done); + }); + }); context("with --only option", () => { - it("should only query the given report", done => { - const queriedReportNames = [] + it("should only query the given report", (done) => { + const queriedReportNames = []; Analytics.query = (report) => { - queriedReportNames.push(report.name) - return Promise.resolve(result) - } - - main.run({ only: "a" }).then(() => { - expect(queriedReportNames).to.include("a") - expect(queriedReportNames).not.to.include.members(["b", "c"]) - done() - }).catch(done) - }) - }) + queriedReportNames.push(report.name); + return Promise.resolve(result); + }; + + main + .run({ only: "a" }) + .then(() => { + expect(queriedReportNames).to.include("a"); + expect(queriedReportNames).not.to.include.members(["b", "c"]); + done(); + }) + .catch(done); + }); + }); context("with --slim option", () => { - it("should format the results with the slim option for slim reports", done => { + it("should format the results with the slim option for slim reports", (done) => { Analytics.reports = [ { name: "a", slim: false }, { name: "b", slim: true }, { name: "c", slim: false }, - ] + ]; - const formattedSlimReportNames = [] - const formattedRegularReportNames = [] + const formattedSlimReportNames = []; + const formattedRegularReportNames = []; ResultFormatter.formatResult = (result, options) => { if (options.slim === true) { - formattedSlimReportNames.push(result.name) + formattedSlimReportNames.push(result.name); } else { - formattedRegularReportNames.push(result.name) + formattedRegularReportNames.push(result.name); } - return Promise.resolve("") - } - - main.run({ slim: true }).then(() => { - expect(formattedSlimReportNames).to.include.members(["b"]) - expect(formattedRegularReportNames).to.include.members(["a", "c"]) - done() - }).catch(done) - }) - }) + return Promise.resolve(""); + }; + + main + .run({ slim: true }) + .then(() => { + expect(formattedSlimReportNames).to.include.members(["b"]); + expect(formattedRegularReportNames).to.include.members(["a", "c"]); + done(); + }) + .catch(done); + }); + }); context("with --csv option", () => { - it("should format the reports with the format set to csv", done => { - const formattedReportNames = [] + it("should format the reports with the format set to csv", (done) => { + const formattedReportNames = []; ResultFormatter.formatResult = (result, options) => { - expect(options.format).to.equal("csv") - formattedReportNames.push(result.name) - return Promise.resolve("") - } - - main.run({ csv: true }).then(() => { - expect(formattedReportNames).to.include.members(["a", "b", "c"]) - done() - }).catch(done) - }) - - it("should publish the reports with the format set to csv", done => { - result = { data: "I'm the result" } - - const publishedReportNames = [] + expect(options.format).to.equal("csv"); + formattedReportNames.push(result.name); + return Promise.resolve(""); + }; + + main + .run({ csv: true }) + .then(() => { + expect(formattedReportNames).to.include.members(["a", "b", "c"]); + done(); + }) + .catch(done); + }); + + it("should publish the reports with the format set to csv", (done) => { + result = { data: "I'm the result" }; + + const publishedReportNames = []; S3Publisher.publish = (report, formattedResult, options) => { - expect(options.format).to.equal("csv") - publishedReportNames.push(report.name) - } - - main.run({ publish: true, csv: true }).then(() => { - expect(publishedReportNames).to.include.members(["a", "b", "c"]) - done() - }).catch(done) - }) - }) + expect(options.format).to.equal("csv"); + publishedReportNames.push(report.name); + }; + + main + .run({ publish: true, csv: true }) + .then(() => { + expect(publishedReportNames).to.include.members(["a", "b", "c"]); + done(); + }) + .catch(done); + }); + }); context("with --frequency option", () => { - it("should only query reports with the given frequency", done => { + it("should only query reports with the given frequency", (done) => { Analytics.reports = [ { name: "a", frequency: "daily" }, { name: "b", frequency: "hourly" }, { name: "c", frequency: "daily" }, - ] + ]; - const queriedReportNames = [] + const queriedReportNames = []; Analytics.query = (report) => { - queriedReportNames.push(report.name) - return Promise.resolve(result) - } - - main.run({ frequency: "daily" }).then(() => { - expect(queriedReportNames).to.include.members(["a", "c"]) - expect(queriedReportNames).not.to.include.members(["b"]) - done() - }).catch(done) - }) - }) - }) -}) + queriedReportNames.push(report.name); + return Promise.resolve(result); + }; + + main + .run({ frequency: "daily" }) + .then(() => { + expect(queriedReportNames).to.include.members(["a", "c"]); + expect(queriedReportNames).not.to.include.members(["b"]); + done(); + }) + .catch(done); + }); + }); + }); +}); diff --git a/test/logger.test.js b/test/logger.test.js index 50d77f8e..151c3f55 100644 --- a/test/logger.test.js +++ b/test/logger.test.js @@ -1,63 +1,68 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); class WinstonConsoleMock { constructor(config) { - this.config = config + this.config = config; } } const winstonMock = { createLogger: (loggerConfig) => { - return loggerConfig + return loggerConfig; }, format: { - combine: (...args) => { return args }, - colorize: () => { return 'colorize' }, - simple: () => { return 'simple' } + combine: (...args) => { + return args; + }, + colorize: () => { + return "colorize"; + }, + simple: () => { + return "simple"; + }, }, transports: { - Console: WinstonConsoleMock - } -} - + Console: WinstonConsoleMock, + }, +}; const logger = proxyquire("../src/logger", { - "winston": winstonMock -}) + winston: winstonMock, +}); describe("logger", () => { describe(".initialize", () => { describe("when ANALYTICS_LOG_LEVEL is set", () => { - const logLevel = 'warn'; + const logLevel = "warn"; beforeEach(() => { process.env.ANALYTICS_LOG_LEVEL = logLevel; - }) + }); afterEach(() => { delete process.env.ANALYTICS_LOG_LEVEL; - }) + }); it("creates a logger with log level set to the environment value", () => { expect(logger.initialize()).to.eql({ level: logLevel, - format: ['colorize', 'simple'], + format: ["colorize", "simple"], transports: [new WinstonConsoleMock({ level: logLevel })], - }) - }) - }) + }); + }); + }); describe("when ANALYTICS_LOG_LEVEL is not set", () => { - const logLevel = 'debug'; + const logLevel = "debug"; it("creates a logger with log level set to debug", () => { expect(logger.initialize()).to.eql({ level: logLevel, - format: ['colorize', 'simple'], + format: ["colorize", "simple"], transports: [new WinstonConsoleMock({ level: logLevel })], - }) - }) - }) - }) -}) + }); + }); + }); + }); +}); diff --git a/test/process-results/ga-data-processor.test.js b/test/process-results/ga-data-processor.test.js index c02d4160..6526244b 100644 --- a/test/process-results/ga-data-processor.test.js +++ b/test/process-results/ga-data-processor.test.js @@ -1,106 +1,121 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") -const reportFixture = require("../support/fixtures/report") -const dataFixture = require("../support/fixtures/data") -const dataWithHostnameFixture = require("../support/fixtures/data_with_hostname") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const reportFixture = require("../support/fixtures/report"); +const dataFixture = require("../support/fixtures/data"); +const dataWithHostnameFixture = require("../support/fixtures/data_with_hostname"); -proxyquire.noCallThru() +proxyquire.noCallThru(); -const config = {} +const config = {}; -const GoogleAnalyticsDataProcessor = proxyquire("../../src/process-results/ga-data-processor", { - "../config": config, -}) +const GoogleAnalyticsDataProcessor = proxyquire( + "../../src/process-results/ga-data-processor", + { + "../config": config, + }, +); describe("GoogleAnalyticsDataProcessor", () => { describe(".processData(report, data)", () => { - let report - let data + let report; + let data; beforeEach(() => { - report = Object.assign({}, reportFixture) - data = Object.assign({}, dataFixture) + report = Object.assign({}, reportFixture); + data = Object.assign({}, dataFixture); config.account = { - hostname: "" - } - }) + hostname: "", + }; + }); it("should return results with the correct props", () => { - const result = GoogleAnalyticsDataProcessor.processData(report, data) - expect(result.name).to.be.a("string") - expect(result.query).be.an("object") - expect(result.meta).be.an("object") - expect(result.data).be.an("array") - expect(result.totals).be.an("object") - expect(result.totals).be.an("object") - expect(result.taken_at).be.a("date") - }) + const result = GoogleAnalyticsDataProcessor.processData(report, data); + expect(result.name).to.be.a("string"); + expect(result.query).be.an("object"); + expect(result.meta).be.an("object"); + expect(result.data).be.an("array"); + expect(result.totals).be.an("object"); + expect(result.totals).be.an("object"); + expect(result.taken_at).be.a("date"); + }); it("should return results with an empty data array if data is undefined or has no rows", () => { - data.rows = [] - expect(GoogleAnalyticsDataProcessor.processData(report, data).data).to.be.empty - data.rows = undefined - expect(GoogleAnalyticsDataProcessor.processData(report, data).data).to.be.empty - }) + data.rows = []; + expect(GoogleAnalyticsDataProcessor.processData(report, data).data).to.be + .empty; + data.rows = undefined; + expect(GoogleAnalyticsDataProcessor.processData(report, data).data).to.be + .empty; + }); it("should delete the query ids for the GA response", () => { - const result = GoogleAnalyticsDataProcessor.processData(report, data) - expect(result.query).to.not.have.property("ids") - }) + const result = GoogleAnalyticsDataProcessor.processData(report, data); + expect(result.query).to.not.have.property("ids"); + }); it("should map headers from GA keys to DAP keys", () => { data.dimensionHeaders = [ - { name: "fileName" }, { name: "operatingSystem" } + { name: "fileName" }, + { name: "operatingSystem" }, ]; - data.metricHeaders = [ - { name: "sessions" }, { name: "activeUsers" } + data.metricHeaders = [{ name: "sessions" }, { name: "activeUsers" }]; + data.rows = [ + { + dimensionValues: [{ value: "foobar" }, { value: "windows" }], + metricValues: [{ value: "12345" }, { value: "23456" }], + }, ]; - data.rows = [{ - dimensionValues: [{ value: "foobar" }, { value: "windows" }], - metricValues: [{ value: "12345" }, { value: "23456" }] - }]; const result = GoogleAnalyticsDataProcessor.processData(report, data); - expect(Object.keys(result.data[0])).to.deep.equal( - ["file_name", "os", "visits", "active_visitors"] - ); - }) + expect(Object.keys(result.data[0])).to.deep.equal([ + "file_name", + "os", + "visits", + "active_visitors", + ]); + }); it("should format dates", () => { - data.dimensionHeaders = [{ name: 'date' }]; + data.dimensionHeaders = [{ name: "date" }]; data.rows = [{ dimensionValues: [{ value: "20170130" }] }]; const result = GoogleAnalyticsDataProcessor.processData(report, data); expect(result.data[0].date).to.equal("2017-01-30"); - }) + }); it("should filter rows that don't meet a dimension threshold if a threshold is provided", () => { report.threshold = { field: "unmapped_column", value: "10", }; - data.dimensionHeaders = [{ name: "operatingSystem" }, { name: "unmapped_column" }]; + data.dimensionHeaders = [ + { name: "operatingSystem" }, + { name: "unmapped_column" }, + ]; data.metricHeaders = [{ name: "sessions" }]; data.rows = [ { dimensionValues: [{ value: "macOs" }, { value: "20" }], - metricValues: [{ value: "12345" }] + metricValues: [{ value: "12345" }], }, { dimensionValues: [{ value: "windows" }, { value: "5" }], - metricValues: [{ value: "12345" }] + metricValues: [{ value: "12345" }], }, { dimensionValues: [{ value: "iOS" }, { value: "15" }], - metricValues: [{ value: "12345" }] - } + metricValues: [{ value: "12345" }], + }, ]; - const result = GoogleAnalyticsDataProcessor.processData(report, data) - expect(result.data).to.have.length(2) - expect(result.data.map(row => row.unmapped_column)).to.deep.equal(["20", "15"]) - }) + const result = GoogleAnalyticsDataProcessor.processData(report, data); + expect(result.data).to.have.length(2); + expect(result.data.map((row) => row.unmapped_column)).to.deep.equal([ + "20", + "15", + ]); + }); it("should filter rows that don't meet a metric threshold if a threshold is provided", () => { report.threshold = { @@ -113,80 +128,99 @@ describe("GoogleAnalyticsDataProcessor", () => { data.rows = [ { dimensionValues: [{ value: "macOs" }], - metricValues: [{ value: "12345" }, { value: "20" }] + metricValues: [{ value: "12345" }, { value: "20" }], }, { dimensionValues: [{ value: "windows" }], - metricValues: [{ value: "12345" }, { value: "5" }] + metricValues: [{ value: "12345" }, { value: "5" }], }, { dimensionValues: [{ value: "iOS" }], - metricValues: [{ value: "12345" }, { value: "15" }] - } + metricValues: [{ value: "12345" }, { value: "15" }], + }, ]; - const result = GoogleAnalyticsDataProcessor.processData(report, data) - expect(result.data).to.have.length(2) - expect(result.data.map(row => row.unmapped_column)).to.deep.equal(["20", "15"]) - }) + const result = GoogleAnalyticsDataProcessor.processData(report, data); + expect(result.data).to.have.length(2); + expect(result.data.map((row) => row.unmapped_column)).to.deep.equal([ + "20", + "15", + ]); + }); it("should remove dimensions that are specified by the cut prop", () => { - report.cut = "unmapped_column" - data.dimensionHeaders = [{ name: "ga:hostname" }, { name: "unmapped_column" }]; + report.cut = "unmapped_column"; + data.dimensionHeaders = [ + { name: "ga:hostname" }, + { name: "unmapped_column" }, + ]; data.metricHeaders = []; - data.rows = [{ - dimensionValues: [{ value: "www.example.gov" }, { value: '10000000' }], - metricValues: [] - }]; + data.rows = [ + { + dimensionValues: [ + { value: "www.example.gov" }, + { value: "10000000" }, + ], + metricValues: [], + }, + ]; const result = GoogleAnalyticsDataProcessor.processData(report, data); expect(result.data[0].unmapped_column).to.be.undefined; - }) + }); it("should remove metrics that are specified by the cut prop", () => { report.cut = "unmapped_column"; - data.dimensionHeaders = [];; + data.dimensionHeaders = []; data.metricHeaders = [{ name: "sessions" }, { name: "unmapped_column" }]; - data.rows = [{ - dimensionValues: [], - metricValues: [{ value: "12345" }, { value: '10000000' }] - }]; + data.rows = [ + { + dimensionValues: [], + metricValues: [{ value: "12345" }, { value: "10000000" }], + }, + ]; const result = GoogleAnalyticsDataProcessor.processData(report, data); expect(result.data[0].unmapped_column).to.be.undefined; - }) + }); it("should add a hostname to realtime data if a hostname is specified by the config", () => { - report.realtime = true - config.account.hostname = "www.example.gov" + report.realtime = true; + config.account.hostname = "www.example.gov"; - const result = GoogleAnalyticsDataProcessor.processData(report, data) - expect(result.data[0].domain).to.equal("www.example.gov") - }) + const result = GoogleAnalyticsDataProcessor.processData(report, data); + expect(result.data[0].domain).to.equal("www.example.gov"); + }); it("should not overwrite the domain with a hostname from the config", () => { - let dataWithHostname - dataWithHostname = Object.assign({}, dataWithHostnameFixture) - report.realtime = true - config.account.hostname = "www.example.gov" - - const result = GoogleAnalyticsDataProcessor.processData(report, dataWithHostname) - expect(result.data[0].domain).to.equal("www.example0.com") - }) + let dataWithHostname; + dataWithHostname = Object.assign({}, dataWithHostnameFixture); + report.realtime = true; + config.account.hostname = "www.example.gov"; + + const result = GoogleAnalyticsDataProcessor.processData( + report, + dataWithHostname, + ); + expect(result.data[0].domain).to.equal("www.example0.com"); + }); it("should set use ResultTotalsCalculator to calculate the totals", () => { const calculateTotals = (result) => { - expect(result.name).to.equal(report.name) - expect(result.data).to.be.an("array") - return { "visits": 1234 } - } - const GoogleAnalyticsDataProcessor = proxyquire("../../src/process-results/ga-data-processor", { - "./config": config, - "./result-totals-calculator": { calculateTotals }, - }) - - const result = GoogleAnalyticsDataProcessor.processData(report, data) - expect(result.totals).to.deep.equal({ "visits": 1234 }) - }) - }) -}) + expect(result.name).to.equal(report.name); + expect(result.data).to.be.an("array"); + return { visits: 1234 }; + }; + const GoogleAnalyticsDataProcessor = proxyquire( + "../../src/process-results/ga-data-processor", + { + "./config": config, + "./result-totals-calculator": { calculateTotals }, + }, + ); + + const result = GoogleAnalyticsDataProcessor.processData(report, data); + expect(result.totals).to.deep.equal({ visits: 1234 }); + }); + }); +}); diff --git a/test/process-results/result-formatter.test.js b/test/process-results/result-formatter.test.js index 550453db..9d190355 100644 --- a/test/process-results/result-formatter.test.js +++ b/test/process-results/result-formatter.test.js @@ -1,56 +1,66 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") -const reportFixture = require("../support/fixtures/report") -const dataFixture = require("../support/fixtures/data") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const reportFixture = require("../support/fixtures/report"); +const dataFixture = require("../support/fixtures/data"); -const GoogleAnalyticsDataProcessor = proxyquire("../../src/process-results/ga-data-processor", { - "../config": { account: { hostname: "" } }, -}) -const ResultFormatter = require("../../src/process-results/result-formatter") +const GoogleAnalyticsDataProcessor = proxyquire( + "../../src/process-results/ga-data-processor", + { + "../config": { account: { hostname: "" } }, + }, +); +const ResultFormatter = require("../../src/process-results/result-formatter"); describe("ResultFormatter", () => { describe("formatResult(result, options)", () => { - let report - let data + let report; + let data; beforeEach(() => { - report = Object.assign({}, reportFixture) - data = Object.assign({}, dataFixture) - }) - - it("should format results into JSON if the format is 'json'", done => { - const result = GoogleAnalyticsDataProcessor.processData(report, data) - - ResultFormatter.formatResult(result, { format: "json" }).then(formattedResult => { - const object = JSON.parse(formattedResult) - expect(object).to.deep.equal(object) - done() - }).catch(done) - }) - - it("should remove the data attribute for JSON if options.slim is true", done => { - const result = GoogleAnalyticsDataProcessor.processData(report, data) - - ResultFormatter.formatResult(result, { format: "json", slim: true }).then(formattedResult => { - const object = JSON.parse(formattedResult) - expect(object.data).to.be.undefined - done() - }).catch(done) - }) + report = Object.assign({}, reportFixture); + data = Object.assign({}, dataFixture); + }); + + it("should format results into JSON if the format is 'json'", (done) => { + const result = GoogleAnalyticsDataProcessor.processData(report, data); + + ResultFormatter.formatResult(result, { format: "json" }) + .then((formattedResult) => { + const object = JSON.parse(formattedResult); + expect(object).to.deep.equal(object); + done(); + }) + .catch(done); + }); + + it("should remove the data attribute for JSON if options.slim is true", (done) => { + const result = GoogleAnalyticsDataProcessor.processData(report, data); + + ResultFormatter.formatResult(result, { format: "json", slim: true }) + .then((formattedResult) => { + const object = JSON.parse(formattedResult); + expect(object.data).to.be.undefined; + done(); + }) + .catch(done); + }); it("should format results into CSV if the format is 'csv'", () => { - const result = GoogleAnalyticsDataProcessor.processData(report, data) + const result = GoogleAnalyticsDataProcessor.processData(report, data); - return ResultFormatter.formatResult(result, { format: "csv", slim: true }).then(formattedResult => { + return ResultFormatter.formatResult(result, { + format: "csv", + slim: true, + }).then((formattedResult) => { const lines = formattedResult.split("\n"); - const [header, ...rows] = lines + const [header, ...rows] = lines; - expect(header).to.equal("date,hour,visits") - rows.forEach(row => { + expect(header).to.equal("date,hour,visits"); + rows.forEach((row) => { // Each CSV row should match 2017-01-30,00,100 - expect(row).to.match(/[0-9]{4}-[0-9]{2}-[0-9]{2},[0-9]{2},100/) + expect(row).to.match(/[0-9]{4}-[0-9]{2}-[0-9]{2},[0-9]{2},100/); }); - }) - }) - }) -}) + }); + }); + }); +}); diff --git a/test/process-results/result-totals-calculator.test.js b/test/process-results/result-totals-calculator.test.js index 5b55653a..4cd73383 100644 --- a/test/process-results/result-totals-calculator.test.js +++ b/test/process-results/result-totals-calculator.test.js @@ -1,14 +1,17 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") -const reportFixture = require("../support/fixtures/report") -const dataFixture = require("../support/fixtures/data") -const ResultTotalsCalculator = require("../../src/process-results/result-totals-calculator") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const reportFixture = require("../support/fixtures/report"); +const dataFixture = require("../support/fixtures/data"); +const ResultTotalsCalculator = require("../../src/process-results/result-totals-calculator"); -proxyquire.noCallThru() +proxyquire.noCallThru(); -const GoogleAnalyticsDataProcessor = proxyquire("../../src/process-results/ga-data-processor", { - "../config": { account: { hostname: "" } }, -}) +const GoogleAnalyticsDataProcessor = proxyquire( + "../../src/process-results/ga-data-processor", + { + "../config": { account: { hostname: "" } }, + }, +); describe("ResultTotalsCalculator", () => { describe("calculateTotals(result)", () => { @@ -26,365 +29,446 @@ describe("ResultTotalsCalculator", () => { const result = GoogleAnalyticsDataProcessor.processData(report, data); const totals = ResultTotalsCalculator.calculateTotals(result); - expect(totals).to.eql({}) - }) - }) + expect(totals).to.eql({}); + }); + }); describe("when the report data is not empty", () => { - let report - let data + let report; + let data; beforeEach(() => { - report = Object.assign({}, reportFixture) - data = Object.assign({}, dataFixture) - }) + report = Object.assign({}, reportFixture); + data = Object.assign({}, dataFixture); + }); it("should compute totals for users", () => { - data.metricHeaders = [{ name: "users" }] + data.metricHeaders = [{ name: "users" }]; data.rows = [ { metricValues: [{ value: "10" }] }, { metricValues: [{ value: "15" }] }, - { metricValues: [{ value: "20" }] } - ] + { metricValues: [{ value: "20" }] }, + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, data) + const result = GoogleAnalyticsDataProcessor.processData(report, data); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.users).to.equal(10 + 15 + 20) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.users).to.equal(10 + 15 + 20); + }); it("should compute totals for visits", () => { - data.metricHeaders = [{ name: "sessions" }] + data.metricHeaders = [{ name: "sessions" }]; data.rows = [ { metricValues: [{ value: "10" }] }, { metricValues: [{ value: "15" }] }, - { metricValues: [{ value: "20" }] } - ] + { metricValues: [{ value: "20" }] }, + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, data) + const result = GoogleAnalyticsDataProcessor.processData(report, data); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.visits).to.equal(10 + 15 + 20) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.visits).to.equal(10 + 15 + 20); + }); it("should compute totals for device_models", () => { - report.name = "device_model" - data.dimensionHeaders = [{ name: "date" }, { name: "mobileDeviceModel" }] - data.metricHeaders = [{ name: "sessions" }] + report.name = "device_model"; + data.dimensionHeaders = [ + { name: "date" }, + { name: "mobileDeviceModel" }, + ]; + data.metricHeaders = [{ name: "sessions" }]; data.rows = [ { dimensionValues: [{ value: "20170130" }, { value: "iPhone" }], - metricValues: [{ value: "100" }] + metricValues: [{ value: "100" }], }, { dimensionValues: [{ value: "20170130" }, { value: "Android" }], - metricValues: [{ value: "200" }] + metricValues: [{ value: "200" }], }, { dimensionValues: [{ value: "20170131" }, { value: "iPhone" }], - metricValues: [{ value: "300" }] + metricValues: [{ value: "300" }], }, { dimensionValues: [{ value: "20170131" }, { value: "Android" }], - metricValues: [{ value: "400" }] - } - ] + metricValues: [{ value: "400" }], + }, + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, data) + const result = GoogleAnalyticsDataProcessor.processData(report, data); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.device_models.iPhone).to.equal(100 + 300) - expect(totals.device_models.Android).to.equal(200 + 400) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.device_models.iPhone).to.equal(100 + 300); + expect(totals.device_models.Android).to.equal(200 + 400); + }); it("should compute totals for languages", () => { - report.name = "language" - data.dimensionHeaders = [{ name: "date" }, { name: "language" }] - data.metricHeaders = [{ name: "sessions" }] + report.name = "language"; + data.dimensionHeaders = [{ name: "date" }, { name: "language" }]; + data.metricHeaders = [{ name: "sessions" }]; data.rows = [ { dimensionValues: [{ value: "20170130" }, { value: "en" }], - metricValues: [{ value: "100" }] + metricValues: [{ value: "100" }], }, { dimensionValues: [{ value: "20170130" }, { value: "es" }], - metricValues: [{ value: "200" }] + metricValues: [{ value: "200" }], }, { dimensionValues: [{ value: "20170131" }, { value: "en" }], - metricValues: [{ value: "300" }] + metricValues: [{ value: "300" }], }, { dimensionValues: [{ value: "20170131" }, { value: "es" }], - metricValues: [{ value: "400" }] - } - ] + metricValues: [{ value: "400" }], + }, + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, data) + const result = GoogleAnalyticsDataProcessor.processData(report, data); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.languages.en).to.equal(100 + 300) - expect(totals.languages.es).to.equal(200 + 400) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.languages.en).to.equal(100 + 300); + expect(totals.languages.es).to.equal(200 + 400); + }); it("should compute totals for devices", () => { - report.name = "devices" - data.dimensionHeaders = [{ name: "date" }, { name: "deviceCategory" }] - data.metricHeaders = [{ name: "sessions" }] + report.name = "devices"; + data.dimensionHeaders = [{ name: "date" }, { name: "deviceCategory" }]; + data.metricHeaders = [{ name: "sessions" }]; data.rows = [ { dimensionValues: [{ value: "20170130" }, { value: "mobile" }], - metricValues: [{ value: "100" }] + metricValues: [{ value: "100" }], }, { dimensionValues: [{ value: "20170130" }, { value: "tablet" }], - metricValues: [{ value: "200" }] + metricValues: [{ value: "200" }], }, { dimensionValues: [{ value: "20170130" }, { value: "desktop" }], - metricValues: [{ value: "300" }] + metricValues: [{ value: "300" }], }, { dimensionValues: [{ value: "20170131" }, { value: "mobile" }], - metricValues: [{ value: "400" }] + metricValues: [{ value: "400" }], }, { dimensionValues: [{ value: "20170131" }, { value: "tablet" }], - metricValues: [{ value: "500" }] + metricValues: [{ value: "500" }], }, { dimensionValues: [{ value: "20170131" }, { value: "desktop" }], - metricValues: [{ value: "600" }] + metricValues: [{ value: "600" }], }, - ] + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, data) + const result = GoogleAnalyticsDataProcessor.processData(report, data); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.devices.mobile).to.equal(100 + 400) - expect(totals.devices.tablet).to.equal(200 + 500) - expect(totals.devices.desktop).to.equal(300 + 600) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.devices.mobile).to.equal(100 + 400); + expect(totals.devices.tablet).to.equal(200 + 500); + expect(totals.devices.desktop).to.equal(300 + 600); + }); it("should compute totals for screen-sizes", () => { - report.name = "screen-size" - data.dimensionHeaders = [{ name: "date" }, { name: "screenResolution" }] - data.metricHeaders = [{ name: "sessions" }] + report.name = "screen-size"; + data.dimensionHeaders = [ + { name: "date" }, + { name: "screenResolution" }, + ]; + data.metricHeaders = [{ name: "sessions" }]; data.rows = [ { dimensionValues: [{ value: "20170130" }, { value: "100x100" }], - metricValues: [{ value: "100" }] + metricValues: [{ value: "100" }], }, { dimensionValues: [{ value: "20170130" }, { value: "200x200" }], - metricValues: [{ value: "200" }] + metricValues: [{ value: "200" }], }, { dimensionValues: [{ value: "20170131" }, { value: "100x100" }], - metricValues: [{ value: "300" }] + metricValues: [{ value: "300" }], }, { dimensionValues: [{ value: "20170131" }, { value: "200x200" }], - metricValues: [{ value: "400" }] - } - ] + metricValues: [{ value: "400" }], + }, + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, data) + const result = GoogleAnalyticsDataProcessor.processData(report, data); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.screen_resolution["100x100"]).to.equal(100 + 300) - expect(totals.screen_resolution["200x200"]).to.equal(200 + 400) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.screen_resolution["100x100"]).to.equal(100 + 300); + expect(totals.screen_resolution["200x200"]).to.equal(200 + 400); + }); it("should compute totals for os", () => { - report.name = "os" - data.dimensionHeaders = [{ name: "date" }, { name: "operatingSystem" }] - data.metricHeaders = [{ name: "sessions" }] + report.name = "os"; + data.dimensionHeaders = [{ name: "date" }, { name: "operatingSystem" }]; + data.metricHeaders = [{ name: "sessions" }]; data.rows = [ { dimensionValues: [{ value: "20170130" }, { value: "Nintendo Wii" }], - metricValues: [{ value: "100" }] + metricValues: [{ value: "100" }], }, { dimensionValues: [{ value: "20170130" }, { value: "Xbox" }], - metricValues: [{ value: "200" }] + metricValues: [{ value: "200" }], }, { dimensionValues: [{ value: "20170131" }, { value: "Nintendo Wii" }], - metricValues: [{ value: "300" }] + metricValues: [{ value: "300" }], }, { dimensionValues: [{ value: "20170131" }, { value: "Xbox" }], - metricValues: [{ value: "400" }] - } - ] + metricValues: [{ value: "400" }], + }, + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, data) + const result = GoogleAnalyticsDataProcessor.processData(report, data); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.os["Nintendo Wii"]).to.equal(100 + 300) - expect(totals.os["Xbox"]).to.equal(200 + 400) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.os["Nintendo Wii"]).to.equal(100 + 300); + expect(totals.os["Xbox"]).to.equal(200 + 400); + }); it("should compute totals for windows", () => { - report.name = "windows" - data.dimensionHeaders = [{ name: "date" }, { name: "operatingSystemVersion" }] - data.metricHeaders = [{ name: "sessions" }] + report.name = "windows"; + data.dimensionHeaders = [ + { name: "date" }, + { name: "operatingSystemVersion" }, + ]; + data.metricHeaders = [{ name: "sessions" }]; data.rows = [ { dimensionValues: [{ value: "20170130" }, { value: "Server" }], - metricValues: [{ value: "100" }] + metricValues: [{ value: "100" }], }, { dimensionValues: [{ value: "20170130" }, { value: "Vista" }], - metricValues: [{ value: "200" }] + metricValues: [{ value: "200" }], }, { dimensionValues: [{ value: "20170131" }, { value: "Server" }], - metricValues: [{ value: "300" }] + metricValues: [{ value: "300" }], }, { dimensionValues: [{ value: "20170131" }, { value: "Vista" }], - metricValues: [{ value: "400" }] - } - ] + metricValues: [{ value: "400" }], + }, + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, data) + const result = GoogleAnalyticsDataProcessor.processData(report, data); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.os_version.Server).to.equal(100 + 300) - expect(totals.os_version.Vista).to.equal(200 + 400) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.os_version.Server).to.equal(100 + 300); + expect(totals.os_version.Vista).to.equal(200 + 400); + }); it("should compute totals for browsers", () => { - report.name = "browsers" - data.dimensionHeaders = [{ name: "date" }, { name: "browser" }] - data.metricHeaders = [{ name: "sessions" }] + report.name = "browsers"; + data.dimensionHeaders = [{ name: "date" }, { name: "browser" }]; + data.metricHeaders = [{ name: "sessions" }]; data.rows = [ { dimensionValues: [{ value: "20170130" }, { value: "Chrome" }], - metricValues: [{ value: "100" }] + metricValues: [{ value: "100" }], }, { dimensionValues: [{ value: "20170130" }, { value: "Safari" }], - metricValues: [{ value: "200" }] + metricValues: [{ value: "200" }], }, { dimensionValues: [{ value: "20170131" }, { value: "Chrome" }], - metricValues: [{ value: "300" }] + metricValues: [{ value: "300" }], }, { dimensionValues: [{ value: "20170131" }, { value: "Safari" }], - metricValues: [{ value: "400" }] - } - ] + metricValues: [{ value: "400" }], + }, + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, data) + const result = GoogleAnalyticsDataProcessor.processData(report, data); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.browser.Chrome).to.equal(100 + 300) - expect(totals.browser.Safari).to.equal(200 + 400) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.browser.Chrome).to.equal(100 + 300); + expect(totals.browser.Safari).to.equal(200 + 400); + }); it("should compute totals for os-browsers by operating system and browser", () => { - report.name = "os-browsers" - data.dimensionHeaders = [{ name: "date" }, { name: "operatingSystem" }, { name: "browser" }] - data.metricHeaders = [{ name: "sessions" }] + report.name = "os-browsers"; + data.dimensionHeaders = [ + { name: "date" }, + { name: "operatingSystem" }, + { name: "browser" }, + ]; + data.metricHeaders = [{ name: "sessions" }]; data.rows = [ { - dimensionValues: [{ value: "20170130" }, { value: "Windows" }, { value: "Chrome" }], - metricValues: [{ value: "100" }] + dimensionValues: [ + { value: "20170130" }, + { value: "Windows" }, + { value: "Chrome" }, + ], + metricValues: [{ value: "100" }], }, { - dimensionValues: [{ value: "20170130" }, { value: "Windows" }, { value: "Firefox" }], - metricValues: [{ value: "200" }] + dimensionValues: [ + { value: "20170130" }, + { value: "Windows" }, + { value: "Firefox" }, + ], + metricValues: [{ value: "200" }], }, { - dimensionValues: [{ value: "20170130" }, { value: "Linux" }, { value: "Chrome" }], - metricValues: [{ value: "300" }] + dimensionValues: [ + { value: "20170130" }, + { value: "Linux" }, + { value: "Chrome" }, + ], + metricValues: [{ value: "300" }], }, { - dimensionValues: [{ value: "20170130" }, { value: "Linux" }, { value: "Firefox" }], - metricValues: [{ value: "400" }] + dimensionValues: [ + { value: "20170130" }, + { value: "Linux" }, + { value: "Firefox" }, + ], + metricValues: [{ value: "400" }], }, { - dimensionValues: [{ value: "20170130" }, { value: "Windows" }, { value: "Chrome" }], - metricValues: [{ value: "500" }] + dimensionValues: [ + { value: "20170130" }, + { value: "Windows" }, + { value: "Chrome" }, + ], + metricValues: [{ value: "500" }], }, { - dimensionValues: [{ value: "20170130" }, { value: "Windows" }, { value: "Firefox" }], - metricValues: [{ value: "600" }] + dimensionValues: [ + { value: "20170130" }, + { value: "Windows" }, + { value: "Firefox" }, + ], + metricValues: [{ value: "600" }], }, { - dimensionValues: [{ value: "20170130" }, { value: "Linux" }, { value: "Chrome" }], - metricValues: [{ value: "700" }] + dimensionValues: [ + { value: "20170130" }, + { value: "Linux" }, + { value: "Chrome" }, + ], + metricValues: [{ value: "700" }], }, { - dimensionValues: [{ value: "20170130" }, { value: "Linux" }, { value: "Firefox" }], - metricValues: [{ value: "800" }] - } - ] + dimensionValues: [ + { value: "20170130" }, + { value: "Linux" }, + { value: "Firefox" }, + ], + metricValues: [{ value: "800" }], + }, + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, data) + const result = GoogleAnalyticsDataProcessor.processData(report, data); - const totals = ResultTotalsCalculator.calculateTotals(result) + const totals = ResultTotalsCalculator.calculateTotals(result); - expect(totals.by_os.Windows.Chrome).to.equal(100 + 500) - expect(totals.by_os.Windows.Firefox).to.equal(200 + 600) + expect(totals.by_os.Windows.Chrome).to.equal(100 + 500); + expect(totals.by_os.Windows.Firefox).to.equal(200 + 600); - expect(totals.by_browsers.Chrome.Windows).to.equal(100 + 500) - expect(totals.by_browsers.Chrome.Linux).to.equal(300 + 700) - }) + expect(totals.by_browsers.Chrome.Windows).to.equal(100 + 500); + expect(totals.by_browsers.Chrome.Linux).to.equal(300 + 700); + }); it("should compute totals for windows-browsers by windows version and browser version", () => { - report.name = "windows-browsers" - data.dimensionHeaders = [{ name: "date" }, { name: "operatingSystemVersion" }, { name: "browser" }] - data.metricHeaders = [{ name: "sessions" }] + report.name = "windows-browsers"; + data.dimensionHeaders = [ + { name: "date" }, + { name: "operatingSystemVersion" }, + { name: "browser" }, + ]; + data.metricHeaders = [{ name: "sessions" }]; data.rows = [ { - dimensionValues: [{ value: "20170130" }, { value: "XP" }, { value: "Chrome" }], - metricValues: [{ value: "100" }] + dimensionValues: [ + { value: "20170130" }, + { value: "XP" }, + { value: "Chrome" }, + ], + metricValues: [{ value: "100" }], }, { - dimensionValues: [{ value: "20170130" }, { value: "XP" }, { value: "Firefox" }], - metricValues: [{ value: "200" }] + dimensionValues: [ + { value: "20170130" }, + { value: "XP" }, + { value: "Firefox" }, + ], + metricValues: [{ value: "200" }], }, { - dimensionValues: [{ value: "20170130" }, { value: "Vista" }, { value: "Chrome" }], - metricValues: [{ value: "300" }] + dimensionValues: [ + { value: "20170130" }, + { value: "Vista" }, + { value: "Chrome" }, + ], + metricValues: [{ value: "300" }], }, { - dimensionValues: [{ value: "20170130" }, { value: "Vista" }, { value: "Firefox" }], - metricValues: [{ value: "400" }] + dimensionValues: [ + { value: "20170130" }, + { value: "Vista" }, + { value: "Firefox" }, + ], + metricValues: [{ value: "400" }], }, { - dimensionValues: [{ value: "20170130" }, { value: "XP" }, { value: "Chrome" }], - metricValues: [{ value: "500" }] + dimensionValues: [ + { value: "20170130" }, + { value: "XP" }, + { value: "Chrome" }, + ], + metricValues: [{ value: "500" }], }, { - dimensionValues: [{ value: "20170130" }, { value: "XP" }, { value: "Firefox" }], - metricValues: [{ value: "600" }] + dimensionValues: [ + { value: "20170130" }, + { value: "XP" }, + { value: "Firefox" }, + ], + metricValues: [{ value: "600" }], }, { - dimensionValues: [{ value: "20170130" }, { value: "Vista" }, { value: "Chrome" }], - metricValues: [{ value: "700" }] + dimensionValues: [ + { value: "20170130" }, + { value: "Vista" }, + { value: "Chrome" }, + ], + metricValues: [{ value: "700" }], }, { - dimensionValues: [{ value: "20170130" }, { value: "Vista" }, { value: "Firefox" }], - metricValues: [{ value: "800" }] - } - ] + dimensionValues: [ + { value: "20170130" }, + { value: "Vista" }, + { value: "Firefox" }, + ], + metricValues: [{ value: "800" }], + }, + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, data) + const result = GoogleAnalyticsDataProcessor.processData(report, data); - const totals = ResultTotalsCalculator.calculateTotals(result) + const totals = ResultTotalsCalculator.calculateTotals(result); - expect(totals.by_windows.XP.Chrome).to.equal(100 + 500) - expect(totals.by_windows.XP.Firefox).to.equal(200 + 600) + expect(totals.by_windows.XP.Chrome).to.equal(100 + 500); + expect(totals.by_windows.XP.Firefox).to.equal(200 + 600); - expect(totals.by_browsers.Chrome.XP).to.equal(100 + 500) - expect(totals.by_browsers.Chrome.Vista).to.equal(300 + 700) - }) - }) - }) -}) + expect(totals.by_browsers.Chrome.XP).to.equal(100 + 500); + expect(totals.by_browsers.Chrome.Vista).to.equal(300 + 700); + }); + }); + }); +}); diff --git a/test/publish/disk.test.js b/test/publish/disk.test.js index 480a73bd..dd927988 100644 --- a/test/publish/disk.test.js +++ b/test/publish/disk.test.js @@ -1,58 +1,66 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); describe("DiskPublisher", () => { - let DiskPublisher - let fs = {} + let DiskPublisher; + let fs = {}; beforeEach(() => { - fs = { writeFile: (path, contents, cb) => cb() } + fs = { + writeFile: async (path, contents) => { + return contents; + }, + }; DiskPublisher = proxyquire("../../src/publish/disk", { - fs: fs, - }) - }) + "node:fs/promises": fs, + }); + }); describe(".publish(report, results, options)", () => { context("when the format is json", () => { - it("should write the results to /.json", done => { - const options = { output: "path/to/output", format: "json" } - const report = { name: "report-name" } - const results = "I'm the results" - - let fileWritten = false - fs.writeFile = (path, contents, cb) => { - expect(path).to.equal("path/to/output/report-name.json") - expect(contents).to.equal("I'm the results") - fileWritten = true - cb(null) - } - - DiskPublisher.publish(report, results, options).then(() => { - expect(fileWritten).to.be.true - done() - }).catch(done) - }) - }) + it("should write the results to /.json", (done) => { + const options = { output: "path/to/output", format: "json" }; + const report = { name: "report-name" }; + const results = "I'm the results"; + + let fileWritten = false; + fs.writeFile = async (path, contents) => { + expect(path).to.equal("path/to/output/report-name.json"); + expect(contents).to.equal("I'm the results"); + fileWritten = true; + return null; + }; + + DiskPublisher.publish(report, results, options) + .then(() => { + expect(fileWritten).to.be.true; + done(); + }) + .catch(done); + }); + }); context("when the format is csv", () => { - it("should write the results to /.csv", done => { - const options = { output: "path/to/output", format: "csv" } - const report = { name: "report-name" } - const results = "I'm the results" - - let fileWritten = false - fs.writeFile = (path, contents, cb) => { - expect(path).to.equal("path/to/output/report-name.csv") - expect(contents).to.equal("I'm the results") - fileWritten = true - cb(null) - } - - DiskPublisher.publish(report, results, options).then(() => { - expect(fileWritten).to.be.true - done() - }).catch(done) - }) - }) - }) -}) + it("should write the results to /.csv", (done) => { + const options = { output: "path/to/output", format: "csv" }; + const report = { name: "report-name" }; + const results = "I'm the results"; + + let fileWritten = false; + fs.writeFile = async (path, contents) => { + expect(path).to.equal("path/to/output/report-name.csv"); + expect(contents).to.equal("I'm the results"); + fileWritten = true; + return null; + }; + + DiskPublisher.publish(report, results, options) + .then(() => { + expect(fileWritten).to.be.true; + done(); + }) + .catch(done); + }); + }); + }); +}); diff --git a/test/publish/postgres.test.js b/test/publish/postgres.test.js index e45523a4..dda28a05 100644 --- a/test/publish/postgres.test.js +++ b/test/publish/postgres.test.js @@ -1,24 +1,24 @@ -const { ANALYTICS_DATA_TABLE_NAME } = require("../../src/publish/postgres") +const { ANALYTICS_DATA_TABLE_NAME } = require("../../src/publish/postgres"); -const expect = require("chai").expect -const knex = require("knex") -const proxyquire = require("proxyquire") -const database = require("../support/database") -const resultsFixture = require("../support/fixtures/results") +const expect = require("chai").expect; +const knex = require("knex"); +const proxyquire = require("proxyquire"); +const database = require("../support/database"); +const resultsFixture = require("../support/fixtures/results"); -proxyquire.noCallThru() +proxyquire.noCallThru(); const PostgresPublisher = proxyquire("../../src/publish/postgres", { - "../config": require('../../src/config'), -}) + "../config": require("../../src/config"), +}); describe("PostgresPublisher", () => { - let databaseClient, results + let databaseClient, results; before((done) => { // Setup the database client - databaseClient = knex({ client: "pg", connection: database.connection }) - done() + databaseClient = knex({ client: "pg", connection: database.connection }); + done(); }); after((done) => { @@ -27,13 +27,13 @@ describe("PostgresPublisher", () => { }); beforeEach((done) => { - results = Object.assign({}, resultsFixture) - database.resetSchema(databaseClient).then(() => done()) - }) + results = Object.assign({}, resultsFixture); + database.resetSchema(databaseClient).then(() => done()); + }); describe(".publish(results)", () => { - it("should insert a record for each results.data element", done => { - results.name = "report-name" + it("should insert a record for each results.data element", (done) => { + results.name = "report-name"; results.data = [ { date: "2017-02-11", @@ -43,62 +43,72 @@ describe("PostgresPublisher", () => { date: "2017-02-12", name: "def", }, - ] - - PostgresPublisher.publish(results).then(() => { - return databaseClient(ANALYTICS_DATA_TABLE_NAME).orderBy("date", "asc").select() - }).then(rows => { - expect(rows).to.have.length(2) - rows.forEach((row, index) => { - const data = results.data[index] - expect(row.report_name).to.equal("report-name") - expect(row.data.name).to.equal(data.name) - expect(row.date.toISOString()).to.match(RegExp(`^${data.date}`)) + ]; + + PostgresPublisher.publish(results) + .then(() => { + return databaseClient(ANALYTICS_DATA_TABLE_NAME) + .orderBy("date", "asc") + .select(); }) - done() - }).catch(done) - }) - - it("should coerce certain values into numbers", done => { - results.name = "report-name" - results.data = [{ - date: "2017-05-15", - name: "abc", - visits: "123", - total_events: "456", - }] - - PostgresPublisher.publish(results).then(() => { - return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME) - }).then(rows => { - const row = rows[0] - expect(row.data.visits).to.be.a("number") - expect(row.data.visits).to.equal(123) - expect(row.data.total_events).to.be.a("number") - expect(row.data.total_events).to.equal(456) - done() - }).catch(done) - }) - - it("should ignore reports that don't have a date dimension", done => { + .then((rows) => { + expect(rows).to.have.length(2); + rows.forEach((row, index) => { + const data = results.data[index]; + expect(row.report_name).to.equal("report-name"); + expect(row.data.name).to.equal(data.name); + expect(row.date.toISOString()).to.match(RegExp(`^${data.date}`)); + }); + done(); + }) + .catch(done); + }); + + it("should coerce certain values into numbers", (done) => { + results.name = "report-name"; + results.data = [ + { + date: "2017-05-15", + name: "abc", + visits: "123", + total_events: "456", + }, + ]; + + PostgresPublisher.publish(results) + .then(() => { + return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME); + }) + .then((rows) => { + const row = rows[0]; + expect(row.data.visits).to.be.a("number"); + expect(row.data.visits).to.equal(123); + expect(row.data.total_events).to.be.a("number"); + expect(row.data.total_events).to.equal(456); + done(); + }) + .catch(done); + }); + + it("should ignore reports that don't have a date dimension", (done) => { results.query = { - dimensions: [ - { "name": "something" }, - { "name": "somethingElse" } - ] - } - - PostgresPublisher.publish(results).then(() => { - return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME) - }).then(rows => { - expect(rows).to.have.length(0) - done() - }).catch(done) - }) - - it("should ignore data points that have already been inserted", done => { - firstResults = Object.assign({}, results) - secondResults = Object.assign({}, results) + dimensions: [{ name: "something" }, { name: "somethingElse" }], + }; + + PostgresPublisher.publish(results) + .then(() => { + return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME); + }) + .then((rows) => { + expect(rows).to.have.length(0); + done(); + }) + .catch(done); + }); + + it("should ignore data points that have already been inserted", (done) => { + const firstResults = Object.assign({}, results); + const secondResults = Object.assign({}, results); firstResults.data = [ { @@ -109,9 +119,9 @@ describe("PostgresPublisher", () => { { date: "2017-02-11", visits: "456", - browser: "Safari" + browser: "Safari", }, - ] + ]; secondResults.data = [ { date: "2017-02-11", @@ -121,23 +131,27 @@ describe("PostgresPublisher", () => { { date: "2017-02-11", visits: "789", - browser: "Internet Explorer" + browser: "Internet Explorer", }, - ] - - PostgresPublisher.publish(firstResults).then(() => { - return PostgresPublisher.publish(secondResults) - }).then(() => { - return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME) - }).then(rows => { - expect(rows).to.have.length(3) - done() - }).catch(done) - }) - - it("should overwrite existing data points if the number of visits or users has changed", done => { - firstResults = Object.assign({}, results) - secondResults = Object.assign({}, results) + ]; + + PostgresPublisher.publish(firstResults) + .then(() => { + return PostgresPublisher.publish(secondResults); + }) + .then(() => { + return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME); + }) + .then((rows) => { + expect(rows).to.have.length(3); + done(); + }) + .catch(done); + }); + + it("should overwrite existing data points if the number of visits or users has changed", (done) => { + const firstResults = Object.assign({}, results); + const secondResults = Object.assign({}, results); firstResults.data = [ { @@ -150,7 +164,7 @@ describe("PostgresPublisher", () => { total_events: "300", title: "IRS Form 123", }, - ] + ]; secondResults.data = [ { date: "2017-02-11", @@ -162,26 +176,30 @@ describe("PostgresPublisher", () => { total_events: "400", title: "IRS Form 123", }, - ] - - PostgresPublisher.publish(firstResults).then(() => { - return PostgresPublisher.publish(secondResults) - }).then(() => { - return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME) - }).then(rows => { - expect(rows).to.have.length(2) - rows.forEach(row => { - if (row.data.visits) { - expect(row.data.visits).to.equal(200) - } else { - expect(row.data.total_events).to.equal(400) - } + ]; + + PostgresPublisher.publish(firstResults) + .then(() => { + return PostgresPublisher.publish(secondResults); + }) + .then(() => { + return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME); }) - done() - }).catch(done) - }) + .then((rows) => { + expect(rows).to.have.length(2); + rows.forEach((row) => { + if (row.data.visits) { + expect(row.data.visits).to.equal(200); + } else { + expect(row.data.total_events).to.equal(400); + } + }); + done(); + }) + .catch(done); + }); - it("should not not insert a record if the date is invalid", done => { + it("should not not insert a record if the date is invalid", (done) => { results.data = [ { date: "(other)", @@ -191,15 +209,18 @@ describe("PostgresPublisher", () => { date: "2017-02-16", visits: "456", }, - ] - - PostgresPublisher.publish(results).then(() => { - return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME) - }).then(rows => { - expect(rows).to.have.length(1) - expect(rows[0].data.visits).to.equal(456) - done() - }).catch(done) - }) - }) -}) + ]; + + PostgresPublisher.publish(results) + .then(() => { + return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME); + }) + .then((rows) => { + expect(rows).to.have.length(1); + expect(rows[0].data.visits).to.equal(456); + done(); + }) + .catch(done); + }); + }); +}); diff --git a/test/publish/s3.test.js b/test/publish/s3.test.js index fedb18cb..f1165991 100644 --- a/test/publish/s3.test.js +++ b/test/publish/s3.test.js @@ -1,7 +1,6 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); const resultsFixture = require("../support/fixtures/results"); -const { should } = require("chai"); let shouldErrorOnSend = false; @@ -12,7 +11,7 @@ class S3ClientMock { send(command) { if (shouldErrorOnSend) { - shouldErrorOnSend = false + shouldErrorOnSend = false; return Promise.reject(command); } else { return Promise.resolve(command); @@ -26,98 +25,112 @@ class PutObjectCommandMock { } } -const zlibMock = {} +const zlibMock = {}; const S3Publisher = proxyquire("../../src/publish/s3", { "@aws-sdk/client-s3": { S3Client: S3ClientMock, - PutObjectCommand: PutObjectCommandMock + PutObjectCommand: PutObjectCommandMock, }, - "zlib": zlibMock, + zlib: zlibMock, "../config": { aws: { bucket: "test-bucket", cache: 60, - path: "path/to/data" + path: "path/to/data", }, }, -}) +}); describe("S3Publisher", () => { - let report - let results + let report; + let results; beforeEach(() => { - results = Object.assign({}, resultsFixture) - report = { name: results.name } - zlibMock.gzip = (data, cb) => cb(null, data) - }) + results = Object.assign({}, resultsFixture); + report = { name: results.name }; + zlibMock.gzip = (data, cb) => cb(null, data); + }); - it("should publish compressed JSON results to the S3 bucket", done => { - report.name = "test-report" - let gzipCalled = false + it("should publish compressed JSON results to the S3 bucket", (done) => { + report.name = "test-report"; + let gzipCalled = false; zlibMock.gzip = (data, cb) => { - gzipCalled = true - cb(null, "compressed data") - } - - S3Publisher.publish(report, `${results}`, { format: "json" }).then((putObjectCommand) => { - expect(putObjectCommand.config.Key).to.equal("path/to/data/test-report.json") - expect(putObjectCommand.config.Bucket).to.equal("test-bucket") - expect(putObjectCommand.config.ContentType).to.equal("application/json") - expect(putObjectCommand.config.ContentEncoding).to.equal("gzip") - expect(putObjectCommand.config.ACL).to.equal("public-read") - expect(putObjectCommand.config.CacheControl).to.equal("max-age=60") - expect(putObjectCommand.config.Body).to.equal("compressed data") - expect(gzipCalled).to.equal(true) - done() - }).catch(done) - }) - - it("should publish compressed CSV results to the S3 bucket", done => { - report.name = "test-report" - let gzipCalled = false + gzipCalled = true; + cb(null, "compressed data"); + }; + + S3Publisher.publish(report, `${results}`, { format: "json" }) + .then((putObjectCommand) => { + expect(putObjectCommand.config.Key).to.equal( + "path/to/data/test-report.json", + ); + expect(putObjectCommand.config.Bucket).to.equal("test-bucket"); + expect(putObjectCommand.config.ContentType).to.equal( + "application/json", + ); + expect(putObjectCommand.config.ContentEncoding).to.equal("gzip"); + expect(putObjectCommand.config.ACL).to.equal("public-read"); + expect(putObjectCommand.config.CacheControl).to.equal("max-age=60"); + expect(putObjectCommand.config.Body).to.equal("compressed data"); + expect(gzipCalled).to.equal(true); + done(); + }) + .catch(done); + }); + + it("should publish compressed CSV results to the S3 bucket", (done) => { + report.name = "test-report"; + let gzipCalled = false; zlibMock.gzip = (data, cb) => { - gzipCalled = true - cb(null, "compressed data") - } - - S3Publisher.publish(report, `${results}`, { format: "csv" }).then((putObjectCommand) => { - expect(putObjectCommand.config.Key).to.equal("path/to/data/test-report.csv") - expect(putObjectCommand.config.Bucket).to.equal("test-bucket") - expect(putObjectCommand.config.ContentType).to.equal("text/csv") - expect(putObjectCommand.config.ContentEncoding).to.equal("gzip") - expect(putObjectCommand.config.ACL).to.equal("public-read") - expect(putObjectCommand.config.CacheControl).to.equal("max-age=60") - expect(putObjectCommand.config.Body).to.equal("compressed data") - expect(gzipCalled).to.equal(true) - done() - }).catch(done) - }) - - it("should reject if there is an error uploading the data", done => { - shouldErrorOnSend = true - let gzipCalled = false + gzipCalled = true; + cb(null, "compressed data"); + }; + + S3Publisher.publish(report, `${results}`, { format: "csv" }) + .then((putObjectCommand) => { + expect(putObjectCommand.config.Key).to.equal( + "path/to/data/test-report.csv", + ); + expect(putObjectCommand.config.Bucket).to.equal("test-bucket"); + expect(putObjectCommand.config.ContentType).to.equal("text/csv"); + expect(putObjectCommand.config.ContentEncoding).to.equal("gzip"); + expect(putObjectCommand.config.ACL).to.equal("public-read"); + expect(putObjectCommand.config.CacheControl).to.equal("max-age=60"); + expect(putObjectCommand.config.Body).to.equal("compressed data"); + expect(gzipCalled).to.equal(true); + done(); + }) + .catch(done); + }); + + it("should reject if there is an error uploading the data", (done) => { + shouldErrorOnSend = true; + let gzipCalled = false; zlibMock.gzip = (data, cb) => { - gzipCalled = true - cb(null, "compressed data") - } - - S3Publisher.publish(report, `${results}`, { format: "json" }).catch(err => { - expect(gzipCalled).to.equal(true) - done() - }).catch(done) - }) - - it("should reject if there is an error compressing the data", done => { - zlibMock.gzip = (data, cb) => cb(new Error("test zlib error")) - - S3Publisher.publish(report.name, `${results}`, { format: "json" }).catch(err => { - expect(err.message).to.equal("test zlib error") - done() - }).catch(done) - }) -}) + gzipCalled = true; + cb(null, "compressed data"); + }; + + S3Publisher.publish(report, `${results}`, { format: "json" }) + .catch(() => { + expect(gzipCalled).to.equal(true); + done(); + }) + .catch(done); + }); + + it("should reject if there is an error compressing the data", (done) => { + zlibMock.gzip = (data, cb) => cb(new Error("test zlib error")); + + S3Publisher.publish(report.name, `${results}`, { format: "json" }) + .catch((err) => { + expect(err.message).to.equal("test zlib error"); + done(); + }) + .catch(done); + }); +}); diff --git a/test/support/database.js b/test/support/database.js index bcdc81bb..7a16f26c 100644 --- a/test/support/database.js +++ b/test/support/database.js @@ -1,9 +1,9 @@ -const { ANALYTICS_DATA_TABLE_NAME } = require("../../src/publish/postgres") +const { ANALYTICS_DATA_TABLE_NAME } = require("../../src/publish/postgres"); -const config = require('../../src/config') +const config = require("../../src/config"); const resetSchema = (db) => { return db(ANALYTICS_DATA_TABLE_NAME).delete(); -} +}; -module.exports = { connection: config.postgres, resetSchema } +module.exports = { connection: config.postgres, resetSchema }; diff --git a/test/support/fixtures/data.js b/test/support/fixtures/data.js index 1e482e44..52a8e61a 100644 --- a/test/support/fixtures/data.js +++ b/test/support/fixtures/data.js @@ -14,12 +14,12 @@ module.exports = { * @param Object[] Describes dimension columns. The number of DimensionHeaders * and ordering of DimensionHeaders matches the dimensions present in rows. */ - dimensionHeaders: [{ name: 'date' }, { name: 'hour' }], + dimensionHeaders: [{ name: "date" }, { name: "hour" }], /** * @param Object[] Describes metric columns. The number of MetricHeaders and * ordering of MetricHeaders matches the metrics present in rows. */ - metricHeaders: [{ name: 'sessions', type: 'TYPE_INTEGER' }], + metricHeaders: [{ name: "sessions", type: "TYPE_INTEGER" }], /** * @param Row[] Rows of dimension value combinations and metric values in the * report. @@ -28,16 +28,16 @@ module.exports = { return { dimensionValues: [ { - value: '20170130', - oneValue: 'value' + value: "20170130", + oneValue: "value", }, { value: `${index}`.length < 2 ? `0${index}` : `${index}`, - oneValue: 'value' - } + oneValue: "value", + }, ], - metricValues: [{ value: `100`, oneValue: 'value' }] - } + metricValues: [{ value: `100`, oneValue: "value" }], + }; }), /** * @param Row[] If requested, the totaled values of metrics. @@ -58,18 +58,18 @@ module.exports = { /** * @param Row[] If requested, the maximum values of metrics. */ - minimums: [], + maximums: [], /** * @param ResponseMetaData metadata carrying additional information about the * report content. */ metadata: { dataLossFromOtherRow: false, - currencyCode: 'USD', - _currencyCode: 'currencyCode', - timeZone: 'America/New_York', - _timeZone: 'timeZone' + currencyCode: "USD", + _currencyCode: "currencyCode", + timeZone: "America/New_York", + _timeZone: "timeZone", }, propertyQuota: null, - kind: 'analyticsData#runReport' -} + kind: "analyticsData#runReport", +}; diff --git a/test/support/fixtures/data_with_hostname.js b/test/support/fixtures/data_with_hostname.js index 376f0232..0fc0c1ca 100644 --- a/test/support/fixtures/data_with_hostname.js +++ b/test/support/fixtures/data_with_hostname.js @@ -1,23 +1,25 @@ module.exports = { - dimensionHeaders: [{ name: 'hostName' }], - metricHeaders: [{ name: 'sessions', type: 'TYPE_INTEGER' }], + dimensionHeaders: [{ name: "hostName" }], + metricHeaders: [{ name: "sessions", type: "TYPE_INTEGER" }], rows: Array.from(Array(24), (_, index) => { return { - dimensionValues: [{ value: `www.example${index}.com`, oneValue: 'value' }], - metricValues: [{ value: `${index}`, oneValue: 'value' }] - } + dimensionValues: [ + { value: `www.example${index}.com`, oneValue: "value" }, + ], + metricValues: [{ value: `${index}`, oneValue: "value" }], + }; }), totals: [], rowCount: 24, minimums: [], - minimums: [], + maximums: [], metadata: { dataLossFromOtherRow: false, - currencyCode: 'USD', - _currencyCode: 'currencyCode', - timeZone: 'America/New_York', - _timeZone: 'timeZone' + currencyCode: "USD", + _currencyCode: "currencyCode", + timeZone: "America/New_York", + _timeZone: "timeZone", }, propertyQuota: null, - kind: 'analyticsData#runReport' -} + kind: "analyticsData#runReport", +}; diff --git a/test/support/fixtures/report.js b/test/support/fixtures/report.js index 767a7435..d6a70ee1 100644 --- a/test/support/fixtures/report.js +++ b/test/support/fixtures/report.js @@ -1,13 +1,13 @@ module.exports = { - "name": "today", - "frequency": "hourly", - "query": { - "dimensions": [{ "name": "date" }, { "name": "hour" }], - "metrics": [{ "name": "sessions" }], - "dateRanges": [{ "startDate": "today", "endDate": "today" }] + name: "today", + frequency: "hourly", + query: { + dimensions: [{ name: "date" }, { name: "hour" }], + metrics: [{ name: "sessions" }], + dateRanges: [{ startDate: "today", endDate: "today" }], }, - "meta": { - "name": "Today", - "description": "Today's visits for all sites." + meta: { + name: "Today", + description: "Today's visits for all sites.", }, -} +}; diff --git a/test/support/fixtures/results.js b/test/support/fixtures/results.js index 98243036..f4d81567 100644 --- a/test/support/fixtures/results.js +++ b/test/support/fixtures/results.js @@ -1,1377 +1,1376 @@ module.exports = { - "name": "devices", - "query": { - "dimensions": [{ "name": "date" }, { "name": "deviceCategory" }], - "metrics": [{ "name": "sessions" }], - "dateRanges": [{ "startDate": "90daysAgo", "endDate": "yesterday" }], - "orderBys": [{ "dimension": { "dimensionName": "date" } }], - "offset": 1, - "limit": 10000 + name: "devices", + query: { + dimensions: [{ name: "date" }, { name: "deviceCategory" }], + metrics: [{ name: "sessions" }], + dateRanges: [{ startDate: "90daysAgo", endDate: "yesterday" }], + orderBys: [{ dimension: { dimensionName: "date" } }], + offset: 1, + limit: 10000, }, - "meta": { - "name": "Devices", - "description": "90 days of desktop/mobile/tablet visits for all sites." + meta: { + name: "Devices", + description: "90 days of desktop/mobile/tablet visits for all sites.", }, - "data": [ + data: [ { - "date": "2016-11-17", - "device": "desktop", - "visits": "17944716" + date: "2016-11-17", + device: "desktop", + visits: "17944716", }, { - "date": "2016-11-17", - "device": "mobile", - "visits": "8927140" + date: "2016-11-17", + device: "mobile", + visits: "8927140", }, { - "date": "2016-11-17", - "device": "tablet", - "visits": "1493615" + date: "2016-11-17", + device: "tablet", + visits: "1493615", }, { - "date": "2016-11-18", - "device": "desktop", - "visits": "15266887" + date: "2016-11-18", + device: "desktop", + visits: "15266887", }, { - "date": "2016-11-18", - "device": "mobile", - "visits": "8188620" + date: "2016-11-18", + device: "mobile", + visits: "8188620", }, { - "date": "2016-11-18", - "device": "tablet", - "visits": "1359252" + date: "2016-11-18", + device: "tablet", + visits: "1359252", }, { - "date": "2016-11-19", - "device": "desktop", - "visits": "7486523" + date: "2016-11-19", + device: "desktop", + visits: "7486523", }, { - "date": "2016-11-19", - "device": "mobile", - "visits": "6802302" + date: "2016-11-19", + device: "mobile", + visits: "6802302", }, { - "date": "2016-11-19", - "device": "tablet", - "visits": "1244910" + date: "2016-11-19", + device: "tablet", + visits: "1244910", }, { - "date": "2016-11-20", - "device": "desktop", - "visits": "8095419" + date: "2016-11-20", + device: "desktop", + visits: "8095419", }, { - "date": "2016-11-20", - "device": "mobile", - "visits": "6355972" + date: "2016-11-20", + device: "mobile", + visits: "6355972", }, { - "date": "2016-11-20", - "device": "tablet", - "visits": "1301498" + date: "2016-11-20", + device: "tablet", + visits: "1301498", }, { - "date": "2016-11-21", - "device": "desktop", - "visits": "18290260" + date: "2016-11-21", + device: "desktop", + visits: "18290260", }, { - "date": "2016-11-21", - "device": "mobile", - "visits": "8660823" + date: "2016-11-21", + device: "mobile", + visits: "8660823", }, { - "date": "2016-11-21", - "device": "tablet", - "visits": "1478005" + date: "2016-11-21", + device: "tablet", + visits: "1478005", }, { - "date": "2016-11-22", - "device": "desktop", - "visits": "16994015" + date: "2016-11-22", + device: "desktop", + visits: "16994015", }, { - "date": "2016-11-22", - "device": "mobile", - "visits": "8599485" + date: "2016-11-22", + device: "mobile", + visits: "8599485", }, { - "date": "2016-11-22", - "device": "tablet", - "visits": "1413091" + date: "2016-11-22", + device: "tablet", + visits: "1413091", }, { - "date": "2016-11-23", - "device": "desktop", - "visits": "13510470" + date: "2016-11-23", + device: "desktop", + visits: "13510470", }, { - "date": "2016-11-23", - "device": "mobile", - "visits": "8133319" + date: "2016-11-23", + device: "mobile", + visits: "8133319", }, { - "date": "2016-11-23", - "device": "tablet", - "visits": "1279496" + date: "2016-11-23", + device: "tablet", + visits: "1279496", }, { - "date": "2016-11-24", - "device": "desktop", - "visits": "6234988" + date: "2016-11-24", + device: "desktop", + visits: "6234988", }, { - "date": "2016-11-24", - "device": "mobile", - "visits": "5953655" + date: "2016-11-24", + device: "mobile", + visits: "5953655", }, { - "date": "2016-11-24", - "device": "tablet", - "visits": "1022314" + date: "2016-11-24", + device: "tablet", + visits: "1022314", }, { - "date": "2016-11-25", - "device": "desktop", - "visits": "8768054" + date: "2016-11-25", + device: "desktop", + visits: "8768054", }, { - "date": "2016-11-25", - "device": "mobile", - "visits": "7241617" + date: "2016-11-25", + device: "mobile", + visits: "7241617", }, { - "date": "2016-11-25", - "device": "tablet", - "visits": "1212316" + date: "2016-11-25", + device: "tablet", + visits: "1212316", }, { - "date": "2016-11-26", - "device": "desktop", - "visits": "6981808" + date: "2016-11-26", + device: "desktop", + visits: "6981808", }, { - "date": "2016-11-26", - "device": "mobile", - "visits": "6722048" + date: "2016-11-26", + device: "mobile", + visits: "6722048", }, { - "date": "2016-11-26", - "device": "tablet", - "visits": "1190519" + date: "2016-11-26", + device: "tablet", + visits: "1190519", }, { - "date": "2016-11-27", - "device": "desktop", - "visits": "8225314" + date: "2016-11-27", + device: "desktop", + visits: "8225314", }, { - "date": "2016-11-27", - "device": "mobile", - "visits": "6672403" + date: "2016-11-27", + device: "mobile", + visits: "6672403", }, { - "date": "2016-11-27", - "device": "tablet", - "visits": "1302649" + date: "2016-11-27", + device: "tablet", + visits: "1302649", }, { - "date": "2016-11-28", - "device": "desktop", - "visits": "19526901" + date: "2016-11-28", + device: "desktop", + visits: "19526901", }, { - "date": "2016-11-28", - "device": "mobile", - "visits": "9300099" + date: "2016-11-28", + device: "mobile", + visits: "9300099", }, { - "date": "2016-11-28", - "device": "tablet", - "visits": "1547016" + date: "2016-11-28", + device: "tablet", + visits: "1547016", }, { - "date": "2016-11-29", - "device": "desktop", - "visits": "19881628" + date: "2016-11-29", + device: "desktop", + visits: "19881628", }, { - "date": "2016-11-29", - "device": "mobile", - "visits": "9665025" + date: "2016-11-29", + device: "mobile", + visits: "9665025", }, { - "date": "2016-11-29", - "device": "tablet", - "visits": "1579273" + date: "2016-11-29", + device: "tablet", + visits: "1579273", }, { - "date": "2016-11-30", - "device": "desktop", - "visits": "19573065" + date: "2016-11-30", + device: "desktop", + visits: "19573065", }, { - "date": "2016-11-30", - "device": "mobile", - "visits": "10083858" + date: "2016-11-30", + device: "mobile", + visits: "10083858", }, { - "date": "2016-11-30", - "device": "tablet", - "visits": "1601741" + date: "2016-11-30", + device: "tablet", + visits: "1601741", }, { - "date": "2016-12-01", - "device": "desktop", - "visits": "18611610" + date: "2016-12-01", + device: "desktop", + visits: "18611610", }, { - "date": "2016-12-01", - "device": "mobile", - "visits": "10212056" + date: "2016-12-01", + device: "mobile", + visits: "10212056", }, { - "date": "2016-12-01", - "device": "tablet", - "visits": "1564647" + date: "2016-12-01", + device: "tablet", + visits: "1564647", }, { - "date": "2016-12-02", - "device": "desktop", - "visits": "16303740" + date: "2016-12-02", + device: "desktop", + visits: "16303740", }, { - "date": "2016-12-02", - "device": "mobile", - "visits": "9595214" + date: "2016-12-02", + device: "mobile", + visits: "9595214", }, { - "date": "2016-12-02", - "device": "tablet", - "visits": "1452885" + date: "2016-12-02", + device: "tablet", + visits: "1452885", }, { - "date": "2016-12-03", - "device": "desktop", - "visits": "8145522" + date: "2016-12-03", + device: "desktop", + visits: "8145522", }, { - "date": "2016-12-03", - "device": "mobile", - "visits": "8038915" + date: "2016-12-03", + device: "mobile", + visits: "8038915", }, { - "date": "2016-12-03", - "device": "tablet", - "visits": "1328963" + date: "2016-12-03", + device: "tablet", + visits: "1328963", }, { - "date": "2016-12-04", - "device": "desktop", - "visits": "8753097" + date: "2016-12-04", + device: "desktop", + visits: "8753097", }, { - "date": "2016-12-04", - "device": "mobile", - "visits": "7206951" + date: "2016-12-04", + device: "mobile", + visits: "7206951", }, { - "date": "2016-12-04", - "device": "tablet", - "visits": "1365981" + date: "2016-12-04", + device: "tablet", + visits: "1365981", }, { - "date": "2016-12-05", - "device": "desktop", - "visits": "20527426" + date: "2016-12-05", + device: "desktop", + visits: "20527426", }, { - "date": "2016-12-05", - "device": "mobile", - "visits": "10433381" + date: "2016-12-05", + device: "mobile", + visits: "10433381", }, { - "date": "2016-12-05", - "device": "tablet", - "visits": "1670167" + date: "2016-12-05", + device: "tablet", + visits: "1670167", }, { - "date": "2016-12-06", - "device": "desktop", - "visits": "19967407" + date: "2016-12-06", + device: "desktop", + visits: "19967407", }, { - "date": "2016-12-06", - "device": "mobile", - "visits": "10023434" + date: "2016-12-06", + device: "mobile", + visits: "10023434", }, { - "date": "2016-12-06", - "device": "tablet", - "visits": "1657519" + date: "2016-12-06", + device: "tablet", + visits: "1657519", }, { - "date": "2016-12-07", - "device": "desktop", - "visits": "19532055" + date: "2016-12-07", + device: "desktop", + visits: "19532055", }, { - "date": "2016-12-07", - "device": "mobile", - "visits": "10063789" + date: "2016-12-07", + device: "mobile", + visits: "10063789", }, { - "date": "2016-12-07", - "device": "tablet", - "visits": "1646568" + date: "2016-12-07", + device: "tablet", + visits: "1646568", }, { - "date": "2016-12-08", - "device": "desktop", - "visits": "19218012" + date: "2016-12-08", + device: "desktop", + visits: "19218012", }, { - "date": "2016-12-08", - "device": "mobile", - "visits": "10323528" + date: "2016-12-08", + device: "mobile", + visits: "10323528", }, { - "date": "2016-12-08", - "device": "tablet", - "visits": "1714556" + date: "2016-12-08", + device: "tablet", + visits: "1714556", }, { - "date": "2016-12-09", - "device": "desktop", - "visits": "16651672" + date: "2016-12-09", + device: "desktop", + visits: "16651672", }, { - "date": "2016-12-09", - "device": "mobile", - "visits": "9478158" + date: "2016-12-09", + device: "mobile", + visits: "9478158", }, { - "date": "2016-12-09", - "device": "tablet", - "visits": "1564344" + date: "2016-12-09", + device: "tablet", + visits: "1564344", }, { - "date": "2016-12-10", - "device": "desktop", - "visits": "8394504" + date: "2016-12-10", + device: "desktop", + visits: "8394504", }, { - "date": "2016-12-10", - "device": "mobile", - "visits": "8008296" + date: "2016-12-10", + device: "mobile", + visits: "8008296", }, { - "date": "2016-12-10", - "device": "tablet", - "visits": "1438817" + date: "2016-12-10", + device: "tablet", + visits: "1438817", }, { - "date": "2016-12-11", - "device": "desktop", - "visits": "8769674" + date: "2016-12-11", + device: "desktop", + visits: "8769674", }, { - "date": "2016-12-11", - "device": "mobile", - "visits": "7318707" + date: "2016-12-11", + device: "mobile", + visits: "7318707", }, { - "date": "2016-12-11", - "device": "tablet", - "visits": "1471781" + date: "2016-12-11", + device: "tablet", + visits: "1471781", }, { - "date": "2016-12-12", - "device": "desktop", - "visits": "20124799" + date: "2016-12-12", + device: "desktop", + visits: "20124799", }, { - "date": "2016-12-12", - "device": "mobile", - "visits": "10002557" + date: "2016-12-12", + device: "mobile", + visits: "10002557", }, { - "date": "2016-12-12", - "device": "tablet", - "visits": "1677637" + date: "2016-12-12", + device: "tablet", + visits: "1677637", }, { - "date": "2016-12-13", - "device": "desktop", - "visits": "19692582" + date: "2016-12-13", + device: "desktop", + visits: "19692582", }, { - "date": "2016-12-13", - "device": "mobile", - "visits": "9946246" + date: "2016-12-13", + device: "mobile", + visits: "9946246", }, { - "date": "2016-12-13", - "device": "tablet", - "visits": "1664839" + date: "2016-12-13", + device: "tablet", + visits: "1664839", }, { - "date": "2016-12-14", - "device": "desktop", - "visits": "19450673" + date: "2016-12-14", + device: "desktop", + visits: "19450673", }, { - "date": "2016-12-14", - "device": "mobile", - "visits": "10324397" + date: "2016-12-14", + device: "mobile", + visits: "10324397", }, { - "date": "2016-12-14", - "device": "tablet", - "visits": "1713116" + date: "2016-12-14", + device: "tablet", + visits: "1713116", }, { - "date": "2016-12-15", - "device": "desktop", - "visits": "19047361" + date: "2016-12-15", + device: "desktop", + visits: "19047361", }, { - "date": "2016-12-15", - "device": "mobile", - "visits": "10346150" + date: "2016-12-15", + device: "mobile", + visits: "10346150", }, { - "date": "2016-12-15", - "device": "tablet", - "visits": "1728800" + date: "2016-12-15", + device: "tablet", + visits: "1728800", }, { - "date": "2016-12-16", - "device": "desktop", - "visits": "16873358" + date: "2016-12-16", + device: "desktop", + visits: "16873358", }, { - "date": "2016-12-16", - "device": "mobile", - "visits": "9932215" + date: "2016-12-16", + device: "mobile", + visits: "9932215", }, { - "date": "2016-12-16", - "device": "tablet", - "visits": "1663874" + date: "2016-12-16", + device: "tablet", + visits: "1663874", }, { - "date": "2016-12-17", - "device": "desktop", - "visits": "8866860" + date: "2016-12-17", + device: "desktop", + visits: "8866860", }, { - "date": "2016-12-17", - "device": "mobile", - "visits": "8772502" + date: "2016-12-17", + device: "mobile", + visits: "8772502", }, { - "date": "2016-12-17", - "device": "tablet", - "visits": "1627369" + date: "2016-12-17", + device: "tablet", + visits: "1627369", }, { - "date": "2016-12-18", - "device": "desktop", - "visits": "8105408" + date: "2016-12-18", + device: "desktop", + visits: "8105408", }, { - "date": "2016-12-18", - "device": "mobile", - "visits": "7414904" + date: "2016-12-18", + device: "mobile", + visits: "7414904", }, { - "date": "2016-12-18", - "device": "tablet", - "visits": "1469536" + date: "2016-12-18", + device: "tablet", + visits: "1469536", }, { - "date": "2016-12-19", - "device": "desktop", - "visits": "19220918" + date: "2016-12-19", + device: "desktop", + visits: "19220918", }, { - "date": "2016-12-19", - "device": "mobile", - "visits": "10438620" + date: "2016-12-19", + device: "mobile", + visits: "10438620", }, { - "date": "2016-12-19", - "device": "tablet", - "visits": "1677447" + date: "2016-12-19", + device: "tablet", + visits: "1677447", }, { - "date": "2016-12-20", - "device": "desktop", - "visits": "18241079" + date: "2016-12-20", + device: "desktop", + visits: "18241079", }, { - "date": "2016-12-20", - "device": "mobile", - "visits": "10558487" + date: "2016-12-20", + device: "mobile", + visits: "10558487", }, { - "date": "2016-12-20", - "device": "tablet", - "visits": "1618781" + date: "2016-12-20", + device: "tablet", + visits: "1618781", }, { - "date": "2016-12-21", - "device": "desktop", - "visits": "17147953" + date: "2016-12-21", + device: "desktop", + visits: "17147953", }, { - "date": "2016-12-21", - "device": "mobile", - "visits": "10422959" + date: "2016-12-21", + device: "mobile", + visits: "10422959", }, { - "date": "2016-12-21", - "device": "tablet", - "visits": "1563992" + date: "2016-12-21", + device: "tablet", + visits: "1563992", }, { - "date": "2016-12-22", - "device": "desktop", - "visits": "15503945" + date: "2016-12-22", + device: "desktop", + visits: "15503945", }, { - "date": "2016-12-22", - "device": "mobile", - "visits": "10305992" + date: "2016-12-22", + device: "mobile", + visits: "10305992", }, { - "date": "2016-12-22", - "device": "tablet", - "visits": "1529405" + date: "2016-12-22", + device: "tablet", + visits: "1529405", }, { - "date": "2016-12-23", - "device": "desktop", - "visits": "11361437" + date: "2016-12-23", + device: "desktop", + visits: "11361437", }, { - "date": "2016-12-23", - "device": "mobile", - "visits": "9521278" + date: "2016-12-23", + device: "mobile", + visits: "9521278", }, { - "date": "2016-12-23", - "device": "tablet", - "visits": "1446075" + date: "2016-12-23", + device: "tablet", + visits: "1446075", }, { - "date": "2016-12-24", - "device": "desktop", - "visits": "5600182" + date: "2016-12-24", + device: "desktop", + visits: "5600182", }, { - "date": "2016-12-24", - "device": "mobile", - "visits": "7144987" + date: "2016-12-24", + device: "mobile", + visits: "7144987", }, { - "date": "2016-12-24", - "device": "tablet", - "visits": "1190168" + date: "2016-12-24", + device: "tablet", + visits: "1190168", }, { - "date": "2016-12-25", - "device": "desktop", - "visits": "4408666" + date: "2016-12-25", + device: "desktop", + visits: "4408666", }, { - "date": "2016-12-25", - "device": "mobile", - "visits": "5531137" + date: "2016-12-25", + device: "mobile", + visits: "5531137", }, { - "date": "2016-12-25", - "device": "tablet", - "visits": "1026063" + date: "2016-12-25", + device: "tablet", + visits: "1026063", }, { - "date": "2016-12-26", - "device": "desktop", - "visits": "7825098" + date: "2016-12-26", + device: "desktop", + visits: "7825098", }, { - "date": "2016-12-26", - "device": "mobile", - "visits": "7232890" + date: "2016-12-26", + device: "mobile", + visits: "7232890", }, { - "date": "2016-12-26", - "device": "tablet", - "visits": "1355893" + date: "2016-12-26", + device: "tablet", + visits: "1355893", }, { - "date": "2016-12-27", - "device": "desktop", - "visits": "13935273" + date: "2016-12-27", + device: "desktop", + visits: "13935273", }, { - "date": "2016-12-27", - "device": "mobile", - "visits": "8975892" + date: "2016-12-27", + device: "mobile", + visits: "8975892", }, { - "date": "2016-12-27", - "device": "tablet", - "visits": "1445369" + date: "2016-12-27", + device: "tablet", + visits: "1445369", }, { - "date": "2016-12-28", - "device": "desktop", - "visits": "14480665" + date: "2016-12-28", + device: "desktop", + visits: "14480665", }, { - "date": "2016-12-28", - "device": "mobile", - "visits": "9244411" + date: "2016-12-28", + device: "mobile", + visits: "9244411", }, { - "date": "2016-12-28", - "device": "tablet", - "visits": "1495648" + date: "2016-12-28", + device: "tablet", + visits: "1495648", }, { - "date": "2016-12-29", - "device": "desktop", - "visits": "14178667" + date: "2016-12-29", + device: "desktop", + visits: "14178667", }, { - "date": "2016-12-29", - "device": "mobile", - "visits": "9223986" + date: "2016-12-29", + device: "mobile", + visits: "9223986", }, { - "date": "2016-12-29", - "device": "tablet", - "visits": "1501026" + date: "2016-12-29", + device: "tablet", + visits: "1501026", }, { - "date": "2016-12-30", - "device": "desktop", - "visits": "11547674" + date: "2016-12-30", + device: "desktop", + visits: "11547674", }, { - "date": "2016-12-30", - "device": "mobile", - "visits": "8372061" + date: "2016-12-30", + device: "mobile", + visits: "8372061", }, { - "date": "2016-12-30", - "device": "tablet", - "visits": "1373276" + date: "2016-12-30", + device: "tablet", + visits: "1373276", }, { - "date": "2016-12-31", - "device": "desktop", - "visits": "6126765" + date: "2016-12-31", + device: "desktop", + visits: "6126765", }, { - "date": "2016-12-31", - "device": "mobile", - "visits": "6393735" + date: "2016-12-31", + device: "mobile", + visits: "6393735", }, { - "date": "2016-12-31", - "device": "tablet", - "visits": "1188851" + date: "2016-12-31", + device: "tablet", + visits: "1188851", }, { - "date": "2017-01-01", - "device": "desktop", - "visits": "5717572" + date: "2017-01-01", + device: "desktop", + visits: "5717572", }, { - "date": "2017-01-01", - "device": "mobile", - "visits": "6002253" + date: "2017-01-01", + device: "mobile", + visits: "6002253", }, { - "date": "2017-01-01", - "device": "tablet", - "visits": "1219702" + date: "2017-01-01", + device: "tablet", + visits: "1219702", }, { - "date": "2017-01-02", - "device": "desktop", - "visits": "10414034" + date: "2017-01-02", + device: "desktop", + visits: "10414034", }, { - "date": "2017-01-02", - "device": "mobile", - "visits": "8280913" + date: "2017-01-02", + device: "mobile", + visits: "8280913", }, { - "date": "2017-01-02", - "device": "tablet", - "visits": "1572182" + date: "2017-01-02", + device: "tablet", + visits: "1572182", }, { - "date": "2017-01-03", - "device": "desktop", - "visits": "19074040" + date: "2017-01-03", + device: "desktop", + visits: "19074040", }, { - "date": "2017-01-03", - "device": "mobile", - "visits": "10002388" + date: "2017-01-03", + device: "mobile", + visits: "10002388", }, { - "date": "2017-01-03", - "device": "tablet", - "visits": "1634073" + date: "2017-01-03", + device: "tablet", + visits: "1634073", }, { - "date": "2017-01-04", - "device": "desktop", - "visits": "19474263" + date: "2017-01-04", + device: "desktop", + visits: "19474263", }, { - "date": "2017-01-04", - "device": "mobile", - "visits": "10263370" + date: "2017-01-04", + device: "mobile", + visits: "10263370", }, { - "date": "2017-01-04", - "device": "tablet", - "visits": "1707684" + date: "2017-01-04", + device: "tablet", + visits: "1707684", }, { - "date": "2017-01-05", - "device": "desktop", - "visits": "19466017" + date: "2017-01-05", + device: "desktop", + visits: "19466017", }, { - "date": "2017-01-05", - "device": "mobile", - "visits": "10736442" + date: "2017-01-05", + device: "mobile", + visits: "10736442", }, { - "date": "2017-01-05", - "device": "tablet", - "visits": "1762507" + date: "2017-01-05", + device: "tablet", + visits: "1762507", }, { - "date": "2017-01-06", - "device": "desktop", - "visits": "17268777" + date: "2017-01-06", + device: "desktop", + visits: "17268777", }, { - "date": "2017-01-06", - "device": "mobile", - "visits": "10204089" + date: "2017-01-06", + device: "mobile", + visits: "10204089", }, { - "date": "2017-01-06", - "device": "tablet", - "visits": "1700304" + date: "2017-01-06", + device: "tablet", + visits: "1700304", }, { - "date": "2017-01-07", - "device": "desktop", - "visits": "8771825" + date: "2017-01-07", + device: "desktop", + visits: "8771825", }, { - "date": "2017-01-07", - "device": "mobile", - "visits": "8622569" + date: "2017-01-07", + device: "mobile", + visits: "8622569", }, { - "date": "2017-01-07", - "device": "tablet", - "visits": "1657525" + date: "2017-01-07", + device: "tablet", + visits: "1657525", }, { - "date": "2017-01-08", - "device": "desktop", - "visits": "8468167" + date: "2017-01-08", + device: "desktop", + visits: "8468167", }, { - "date": "2017-01-08", - "device": "mobile", - "visits": "7523797" + date: "2017-01-08", + device: "mobile", + visits: "7523797", }, { - "date": "2017-01-08", - "device": "tablet", - "visits": "1573548" + date: "2017-01-08", + device: "tablet", + visits: "1573548", }, { - "date": "2017-01-09", - "device": "desktop", - "visits": "19946515" + date: "2017-01-09", + device: "desktop", + visits: "19946515", }, { - "date": "2017-01-09", - "device": "mobile", - "visits": "10112103" + date: "2017-01-09", + device: "mobile", + visits: "10112103", }, { - "date": "2017-01-09", - "device": "tablet", - "visits": "1724557" + date: "2017-01-09", + device: "tablet", + visits: "1724557", }, { - "date": "2017-01-10", - "device": "desktop", - "visits": "20321640" + date: "2017-01-10", + device: "desktop", + visits: "20321640", }, { - "date": "2017-01-10", - "device": "mobile", - "visits": "10515776" + date: "2017-01-10", + device: "mobile", + visits: "10515776", }, { - "date": "2017-01-10", - "device": "tablet", - "visits": "1795632" + date: "2017-01-10", + device: "tablet", + visits: "1795632", }, { - "date": "2017-01-11", - "device": "desktop", - "visits": "19671577" + date: "2017-01-11", + device: "desktop", + visits: "19671577", }, { - "date": "2017-01-11", - "device": "mobile", - "visits": "10465313" + date: "2017-01-11", + device: "mobile", + visits: "10465313", }, { - "date": "2017-01-11", - "device": "tablet", - "visits": "1732368" + date: "2017-01-11", + device: "tablet", + visits: "1732368", }, { - "date": "2017-01-12", - "device": "desktop", - "visits": "19589937" + date: "2017-01-12", + device: "desktop", + visits: "19589937", }, { - "date": "2017-01-12", - "device": "mobile", - "visits": "10277052" + date: "2017-01-12", + device: "mobile", + visits: "10277052", }, { - "date": "2017-01-12", - "device": "tablet", - "visits": "1703584" + date: "2017-01-12", + device: "tablet", + visits: "1703584", }, { - "date": "2017-01-13", - "device": "desktop", - "visits": "17146743" + date: "2017-01-13", + device: "desktop", + visits: "17146743", }, { - "date": "2017-01-13", - "device": "mobile", - "visits": "9619211" + date: "2017-01-13", + device: "mobile", + visits: "9619211", }, { - "date": "2017-01-13", - "device": "tablet", - "visits": "1585216" + date: "2017-01-13", + device: "tablet", + visits: "1585216", }, { - "date": "2017-01-14", - "device": "desktop", - "visits": "8330783" + date: "2017-01-14", + device: "desktop", + visits: "8330783", }, { - "date": "2017-01-14", - "device": "mobile", - "visits": "8038168" + date: "2017-01-14", + device: "mobile", + visits: "8038168", }, { - "date": "2017-01-14", - "device": "tablet", - "visits": "1474055" + date: "2017-01-14", + device: "tablet", + visits: "1474055", }, { - "date": "2017-01-15", - "device": "desktop", - "visits": "7940108" + date: "2017-01-15", + device: "desktop", + visits: "7940108", }, { - "date": "2017-01-15", - "device": "mobile", - "visits": "7377663" + date: "2017-01-15", + device: "mobile", + visits: "7377663", }, { - "date": "2017-01-15", - "device": "tablet", - "visits": "1420365" + date: "2017-01-15", + device: "tablet", + visits: "1420365", }, { - "date": "2017-01-16", - "device": "desktop", - "visits": "14829426" + date: "2017-01-16", + device: "desktop", + visits: "14829426", }, { - "date": "2017-01-16", - "device": "mobile", - "visits": "9257283" + date: "2017-01-16", + device: "mobile", + visits: "9257283", }, { - "date": "2017-01-16", - "device": "tablet", - "visits": "1558470" + date: "2017-01-16", + device: "tablet", + visits: "1558470", }, { - "date": "2017-01-17", - "device": "desktop", - "visits": "21076771" + date: "2017-01-17", + device: "desktop", + visits: "21076771", }, { - "date": "2017-01-17", - "device": "mobile", - "visits": "11441390" + date: "2017-01-17", + device: "mobile", + visits: "11441390", }, { - "date": "2017-01-17", - "device": "tablet", - "visits": "1742698" + date: "2017-01-17", + device: "tablet", + visits: "1742698", }, { - "date": "2017-01-18", - "device": "desktop", - "visits": "20446130" + date: "2017-01-18", + device: "desktop", + visits: "20446130", }, { - "date": "2017-01-18", - "device": "mobile", - "visits": "10970693" + date: "2017-01-18", + device: "mobile", + visits: "10970693", }, { - "date": "2017-01-18", - "device": "tablet", - "visits": "1717717" + date: "2017-01-18", + device: "tablet", + visits: "1717717", }, { - "date": "2017-01-19", - "device": "desktop", - "visits": "20157052" + date: "2017-01-19", + device: "desktop", + visits: "20157052", }, { - "date": "2017-01-19", - "device": "mobile", - "visits": "11228989" + date: "2017-01-19", + device: "mobile", + visits: "11228989", }, { - "date": "2017-01-19", - "device": "tablet", - "visits": "1726224" + date: "2017-01-19", + device: "tablet", + visits: "1726224", }, { - "date": "2017-01-20", - "device": "desktop", - "visits": "19344217" + date: "2017-01-20", + device: "desktop", + visits: "19344217", }, { - "date": "2017-01-20", - "device": "mobile", - "visits": "12884804" + date: "2017-01-20", + device: "mobile", + visits: "12884804", }, { - "date": "2017-01-20", - "device": "tablet", - "visits": "1873116" + date: "2017-01-20", + device: "tablet", + visits: "1873116", }, { - "date": "2017-01-21", - "device": "desktop", - "visits": "9950647" + date: "2017-01-21", + device: "desktop", + visits: "9950647", }, { - "date": "2017-01-21", - "device": "mobile", - "visits": "10568161" + date: "2017-01-21", + device: "mobile", + visits: "10568161", }, { - "date": "2017-01-21", - "device": "tablet", - "visits": "1783297" + date: "2017-01-21", + device: "tablet", + visits: "1783297", }, { - "date": "2017-01-22", - "device": "desktop", - "visits": "10151644" + date: "2017-01-22", + device: "desktop", + visits: "10151644", }, { - "date": "2017-01-22", - "device": "mobile", - "visits": "9316374" + date: "2017-01-22", + device: "mobile", + visits: "9316374", }, { - "date": "2017-01-22", - "device": "tablet", - "visits": "1822457" + date: "2017-01-22", + device: "tablet", + visits: "1822457", }, { - "date": "2017-01-23", - "device": "desktop", - "visits": "23257771" + date: "2017-01-23", + device: "desktop", + visits: "23257771", }, { - "date": "2017-01-23", - "device": "mobile", - "visits": "12281874" + date: "2017-01-23", + device: "mobile", + visits: "12281874", }, { - "date": "2017-01-23", - "device": "tablet", - "visits": "1957768" + date: "2017-01-23", + device: "tablet", + visits: "1957768", }, { - "date": "2017-01-24", - "device": "desktop", - "visits": "21802654" + date: "2017-01-24", + device: "desktop", + visits: "21802654", }, { - "date": "2017-01-24", - "device": "mobile", - "visits": "11787571" + date: "2017-01-24", + device: "mobile", + visits: "11787571", }, { - "date": "2017-01-24", - "device": "tablet", - "visits": "1840512" + date: "2017-01-24", + device: "tablet", + visits: "1840512", }, { - "date": "2017-01-25", - "device": "desktop", - "visits": "21217961" + date: "2017-01-25", + device: "desktop", + visits: "21217961", }, { - "date": "2017-01-25", - "device": "mobile", - "visits": "12259488" + date: "2017-01-25", + device: "mobile", + visits: "12259488", }, { - "date": "2017-01-25", - "device": "tablet", - "visits": "1824556" + date: "2017-01-25", + device: "tablet", + visits: "1824556", }, { - "date": "2017-01-26", - "device": "desktop", - "visits": "20151178" + date: "2017-01-26", + device: "desktop", + visits: "20151178", }, { - "date": "2017-01-26", - "device": "mobile", - "visits": "11692776" + date: "2017-01-26", + device: "mobile", + visits: "11692776", }, { - "date": "2017-01-26", - "device": "tablet", - "visits": "1720242" + date: "2017-01-26", + device: "tablet", + visits: "1720242", }, { - "date": "2017-01-27", - "device": "desktop", - "visits": "17657726" + date: "2017-01-27", + device: "desktop", + visits: "17657726", }, { - "date": "2017-01-27", - "device": "mobile", - "visits": "10761667" + date: "2017-01-27", + device: "mobile", + visits: "10761667", }, { - "date": "2017-01-27", - "device": "tablet", - "visits": "1574402" + date: "2017-01-27", + device: "tablet", + visits: "1574402", }, { - "date": "2017-01-28", - "device": "desktop", - "visits": "9175780" + date: "2017-01-28", + device: "desktop", + visits: "9175780", }, { - "date": "2017-01-28", - "device": "mobile", - "visits": "9316210" + date: "2017-01-28", + device: "mobile", + visits: "9316210", }, { - "date": "2017-01-28", - "device": "tablet", - "visits": "1486173" + date: "2017-01-28", + device: "tablet", + visits: "1486173", }, { - "date": "2017-01-29", - "device": "desktop", - "visits": "9761406" + date: "2017-01-29", + device: "desktop", + visits: "9761406", }, { - "date": "2017-01-29", - "device": "mobile", - "visits": "9702597" + date: "2017-01-29", + device: "mobile", + visits: "9702597", }, { - "date": "2017-01-29", - "device": "tablet", - "visits": "1606222" + date: "2017-01-29", + device: "tablet", + visits: "1606222", }, { - "date": "2017-01-30", - "device": "desktop", - "visits": "22638067" + date: "2017-01-30", + device: "desktop", + visits: "22638067", }, { - "date": "2017-01-30", - "device": "mobile", - "visits": "12653369" + date: "2017-01-30", + device: "mobile", + visits: "12653369", }, { - "date": "2017-01-30", - "device": "tablet", - "visits": "1858651" + date: "2017-01-30", + device: "tablet", + visits: "1858651", }, { - "date": "2017-01-31", - "device": "desktop", - "visits": "22251428" + date: "2017-01-31", + device: "desktop", + visits: "22251428", }, { - "date": "2017-01-31", - "device": "mobile", - "visits": "12268125" + date: "2017-01-31", + device: "mobile", + visits: "12268125", }, { - "date": "2017-01-31", - "device": "tablet", - "visits": "1819209" + date: "2017-01-31", + device: "tablet", + visits: "1819209", }, { - "date": "2017-02-01", - "device": "desktop", - "visits": "21087290" + date: "2017-02-01", + device: "desktop", + visits: "21087290", }, { - "date": "2017-02-01", - "device": "mobile", - "visits": "12257163" + date: "2017-02-01", + device: "mobile", + visits: "12257163", }, { - "date": "2017-02-01", - "device": "tablet", - "visits": "1791769" + date: "2017-02-01", + device: "tablet", + visits: "1791769", }, { - "date": "2017-02-02", - "device": "desktop", - "visits": "20524207" + date: "2017-02-02", + device: "desktop", + visits: "20524207", }, { - "date": "2017-02-02", - "device": "mobile", - "visits": "12114547" + date: "2017-02-02", + device: "mobile", + visits: "12114547", }, { - "date": "2017-02-02", - "device": "tablet", - "visits": "1757504" + date: "2017-02-02", + device: "tablet", + visits: "1757504", }, { - "date": "2017-02-03", - "device": "desktop", - "visits": "17997793" + date: "2017-02-03", + device: "desktop", + visits: "17997793", }, { - "date": "2017-02-03", - "device": "mobile", - "visits": "11483512" + date: "2017-02-03", + device: "mobile", + visits: "11483512", }, { - "date": "2017-02-03", - "device": "tablet", - "visits": "1646621" + date: "2017-02-03", + device: "tablet", + visits: "1646621", }, { - "date": "2017-02-04", - "device": "desktop", - "visits": "9313172" + date: "2017-02-04", + device: "desktop", + visits: "9313172", }, { - "date": "2017-02-04", - "device": "mobile", - "visits": "9544262" + date: "2017-02-04", + device: "mobile", + visits: "9544262", }, { - "date": "2017-02-04", - "device": "tablet", - "visits": "1503310" + date: "2017-02-04", + device: "tablet", + visits: "1503310", }, { - "date": "2017-02-05", - "device": "desktop", - "visits": "8833525" + date: "2017-02-05", + device: "desktop", + visits: "8833525", }, { - "date": "2017-02-05", - "device": "mobile", - "visits": "8273273" + date: "2017-02-05", + device: "mobile", + visits: "8273273", }, { - "date": "2017-02-05", - "device": "tablet", - "visits": "1436846" + date: "2017-02-05", + device: "tablet", + visits: "1436846", }, { - "date": "2017-02-06", - "device": "desktop", - "visits": "21775734" + date: "2017-02-06", + device: "desktop", + visits: "21775734", }, { - "date": "2017-02-06", - "device": "mobile", - "visits": "12223955" + date: "2017-02-06", + device: "mobile", + visits: "12223955", }, { - "date": "2017-02-06", - "device": "tablet", - "visits": "1821893" + date: "2017-02-06", + device: "tablet", + visits: "1821893", }, { - "date": "2017-02-07", - "device": "desktop", - "visits": "22100599" + date: "2017-02-07", + device: "desktop", + visits: "22100599", }, { - "date": "2017-02-07", - "device": "mobile", - "visits": "12625240" + date: "2017-02-07", + device: "mobile", + visits: "12625240", }, { - "date": "2017-02-07", - "device": "tablet", - "visits": "1899859" + date: "2017-02-07", + device: "tablet", + visits: "1899859", }, { - "date": "2017-02-08", - "device": "desktop", - "visits": "22031758" + date: "2017-02-08", + device: "desktop", + visits: "22031758", }, { - "date": "2017-02-08", - "device": "mobile", - "visits": "13262193" + date: "2017-02-08", + device: "mobile", + visits: "13262193", }, { - "date": "2017-02-08", - "device": "tablet", - "visits": "1931228" + date: "2017-02-08", + device: "tablet", + visits: "1931228", }, { - "date": "2017-02-09", - "device": "desktop", - "visits": "20575032" + date: "2017-02-09", + device: "desktop", + visits: "20575032", }, { - "date": "2017-02-09", - "device": "mobile", - "visits": "12979335" + date: "2017-02-09", + device: "mobile", + visits: "12979335", }, { - "date": "2017-02-09", - "device": "tablet", - "visits": "1921387" + date: "2017-02-09", + device: "tablet", + visits: "1921387", }, { - "date": "2017-02-10", - "device": "desktop", - "visits": "17711813" + date: "2017-02-10", + device: "desktop", + visits: "17711813", }, { - "date": "2017-02-10", - "device": "mobile", - "visits": "11965905" + date: "2017-02-10", + device: "mobile", + visits: "11965905", }, { - "date": "2017-02-10", - "device": "tablet", - "visits": "1675788" + date: "2017-02-10", + device: "tablet", + visits: "1675788", }, { - "date": "2017-02-11", - "device": "desktop", - "visits": "9097741" + date: "2017-02-11", + device: "desktop", + visits: "9097741", }, { - "date": "2017-02-11", - "device": "mobile", - "visits": "10059393" + date: "2017-02-11", + device: "mobile", + visits: "10059393", }, { - "date": "2017-02-11", - "device": "tablet", - "visits": "1542236" + date: "2017-02-11", + device: "tablet", + visits: "1542236", }, { - "date": "2017-02-12", - "device": "desktop", - "visits": "9652936" + date: "2017-02-12", + device: "desktop", + visits: "9652936", }, { - "date": "2017-02-12", - "device": "mobile", - "visits": "9133410" + date: "2017-02-12", + device: "mobile", + visits: "9133410", }, { - "date": "2017-02-12", - "device": "tablet", - "visits": "1592009" + date: "2017-02-12", + device: "tablet", + visits: "1592009", }, { - "date": "2017-02-13", - "device": "desktop", - "visits": "20780584" + date: "2017-02-13", + device: "desktop", + visits: "20780584", }, { - "date": "2017-02-13", - "device": "mobile", - "visits": "12435261" + date: "2017-02-13", + device: "mobile", + visits: "12435261", }, { - "date": "2017-02-13", - "device": "tablet", - "visits": "1753516" + date: "2017-02-13", + device: "tablet", + visits: "1753516", }, { - "date": "2017-02-14", - "device": "desktop", - "visits": "19207139" + date: "2017-02-14", + device: "desktop", + visits: "19207139", }, { - "date": "2017-02-14", - "device": "mobile", - "visits": "11879814" + date: "2017-02-14", + device: "mobile", + visits: "11879814", }, { - "date": "2017-02-14", - "device": "tablet", - "visits": "1642179" - } + date: "2017-02-14", + device: "tablet", + visits: "1642179", + }, ], - "totals": { - "visits": 2380289500, - "devices": { - "desktop": 1369555309, - "mobile": 868783942, - "tablet": 141950249 - } + totals: { + visits: 2380289500, + devices: { + desktop: 1369555309, + mobile: 868783942, + tablet: 141950249, + }, }, - "taken_at": "2017-02-15T15:44:53.044Z" -} - + taken_at: "2017-02-15T15:44:53.044Z", +}; diff --git a/test/support/mocks/googleapis-analytics.js b/test/support/mocks/googleapis-analytics.js index 535abb4e..5613d5c3 100644 --- a/test/support/mocks/googleapis-analytics.js +++ b/test/support/mocks/googleapis-analytics.js @@ -1,18 +1,18 @@ -const dataFixture = require("../fixtures/data") +const dataFixture = require("../fixtures/data"); const googleAPIsMock = () => { - const data = Object.assign({}, dataFixture) - const realtime = { get: (query, callback) => callback(null, data) } - const ga = { get: (query, callback) => callback(null, data) } + const data = Object.assign({}, dataFixture); + const realtime = { get: (query, callback) => callback(null, data) }; + const ga = { get: (query, callback) => callback(null, data) }; - const analytics = (() => ({ + const analytics = () => ({ data: { realtime: realtime, ga: ga, - } - })) + }, + }); - return { realtime, ga, analytics } -} + return { realtime, ga, analytics }; +}; -module.exports = googleAPIsMock +module.exports = googleAPIsMock; diff --git a/test/support/mocks/googleapis-auth.js b/test/support/mocks/googleapis-auth.js index f6a2bf72..b9db9c09 100644 --- a/test/support/mocks/googleapis-auth.js +++ b/test/support/mocks/googleapis-auth.js @@ -1,11 +1,9 @@ -const dataFixture = require("../fixtures/data") - const googleAPIsMock = () => { function JWT() { - this.initArguments = arguments + this.initArguments = arguments; } - JWT.prototype.authorize = (callback) => callback(null, {}) - return { Auth: { JWT } } -} + JWT.prototype.authorize = (callback) => callback(null, {}); + return { Auth: { JWT } }; +}; -module.exports = googleAPIsMock +module.exports = googleAPIsMock; diff --git a/ua/index.js b/ua/index.js index 60183b7d..cafc6ae4 100644 --- a/ua/index.js +++ b/ua/index.js @@ -1,29 +1,30 @@ -const config = require("./src/config") -const Analytics = require("./src/analytics") -const DiskPublisher = require("./src/publish/disk") -const PostgresPublisher = require("./src/publish/postgres") -const ResultFormatter = require("./src/process-results/result-formatter") -const logger = require('../src/logger').initialize(); +const config = require("./src/config"); +const Analytics = require("./src/analytics"); +const DiskPublisher = require("./src/publish/disk"); +const PostgresPublisher = require("./src/publish/postgres"); +const ResultFormatter = require("./src/process-results/result-formatter"); +const Logger = require("../src/logger"); +const logger = Logger.initialize(); Promise.each = async function (arr, fn) { for (const item of arr) await fn(item); -} +}; const run = function (options = {}) { - const reports = _filterReports(options) - return Promise.each(reports, report => _runReport(report, options)) -} + const reports = _filterReports(options); + return Promise.each(reports, (report) => _runReport(report, options)); +}; const _filterReports = ({ only, frequency }) => { - const reports = Analytics.reports + const reports = Analytics.reports; if (only) { - return reports.filter(report => report.name === only) + return reports.filter((report) => report.name === only); } else if (frequency) { - return reports.filter(report => report.frequency === frequency) + return reports.filter((report) => report.frequency === frequency); } else { - return reports + return reports; } -} +}; const _optionsForReport = (report, options) => ({ format: options.csv ? "csv" : "json", @@ -32,42 +33,46 @@ const _optionsForReport = (report, options) => ({ realtime: report.realtime, slim: options.slim && report.slim, writeToDatabase: options["write-to-database"], -}) +}); const _publishReport = (report, formattedResult, options) => { - logger.debug(`[${report.name}]`, "Publishing...") - if (options.output && typeof (options.output) === "string") { - return DiskPublisher.publish(report, formattedResult, options) + logger.debug(`${Logger.tag(report.name)} Publishing...`); + if (options.output && typeof options.output === "string") { + return DiskPublisher.publish(report, formattedResult, options); } else { - console.log(formattedResult) + console.log(formattedResult); } -} +}; const _runReport = (report, options) => { - const reportOptions = _optionsForReport(report, options) - logger.debug("[" + report.name + "] Fetching..."); + const reportOptions = _optionsForReport(report, options); + logger.debug(`${Logger.tag(report.name)} Fetching...`); - return Analytics.query(report).then(results => { - logger.debug("[" + report.name + "] Saving report data...") - if (config.account.agency_name) { - results.agency = config.account.agency_name - } - return _writeReportToDatabase(report, results, options) - }).then(results => { - return ResultFormatter.formatResult(results, reportOptions) - }).then(formattedResult => { - return _publishReport(report, formattedResult, reportOptions) - }).catch(err => { - logger.error(`[${report.name}] `, err) - }) -} + return Analytics.query(report) + .then((results) => { + logger.debug(`${Logger.tag(report.name)} Saving report data...`); + if (config.account.agency_name) { + results.agency = config.account.agency_name; + } + return _writeReportToDatabase(report, results, options); + }) + .then((results) => { + return ResultFormatter.formatResult(results, reportOptions); + }) + .then((formattedResult) => { + return _publishReport(report, formattedResult, reportOptions); + }) + .catch((err) => { + logger.error(`[${report.name}] `, err); + }); +}; const _writeReportToDatabase = (report, result, options) => { if (options["write-to-database"] && !report.realtime) { - return PostgresPublisher.publish(result).then(() => result) + return PostgresPublisher.publish(result).then(() => result); } else { - return Promise.resolve(result) + return Promise.resolve(result); } -} +}; module.exports = { run }; diff --git a/ua/src/analytics.js b/ua/src/analytics.js index 63343434..46660111 100755 --- a/ua/src/analytics.js +++ b/ua/src/analytics.js @@ -1,24 +1,29 @@ -const path = require("path") -const config = require('./config') -const GoogleAnalyticsClient = require("./google-analytics/client") -const GoogleAnalyticsDataProcessor = require("./process-results/ga-data-processor") +const path = require("path"); +const config = require("./config"); +const GoogleAnalyticsClient = require("./google-analytics/client"); +const GoogleAnalyticsDataProcessor = require("./process-results/ga-data-processor"); const query = (report) => { if (!report) { - return Promise.reject(new Error("Analytics.query missing required argument `report`")) + return Promise.reject( + new Error("Analytics.query missing required argument `report`"), + ); } - return GoogleAnalyticsClient.fetchData(report).then(data => { - return GoogleAnalyticsDataProcessor.processData(report, data) - }) -} + return GoogleAnalyticsClient.fetchData(report).then((data) => { + return GoogleAnalyticsDataProcessor.processData(report, data); + }); +}; const _loadReports = () => { - const _reportFilePath = path.resolve(process.cwd(), config.ua_reports_file || "reports/reports.json") - return require(_reportFilePath).reports -} + const _reportFilePath = path.resolve( + process.cwd(), + config.ua_reports_file || "reports/reports.json", + ); + return require(_reportFilePath).reports; +}; module.exports = { query, reports: _loadReports(), -} +}; diff --git a/ua/src/config.js b/ua/src/config.js index 657388a6..bc46ed6f 100644 --- a/ua/src/config.js +++ b/ua/src/config.js @@ -1,4 +1,4 @@ -const knexfile = require('../../knexfile'); +const knexfile = require("../../knexfile"); // Set environment variables to configure the application. module.exports = { @@ -6,7 +6,7 @@ module.exports = { key: process.env.ANALYTICS_KEY, analytics_credentials: process.env.ANALYTICS_CREDENTIALS, ua_reports_file: process.env.ANALYTICS_UA_REPORTS_PATH, - debug: (process.env.ANALYTICS_DEBUG ? true : false), + debug: process.env.ANALYTICS_DEBUG ? true : false, account: { ids: process.env.ANALYTICS_REPORT_UA_IDS, agency_name: process.env.AGENCY_NAME, @@ -16,6 +16,6 @@ module.exports = { }, postgres: knexfile[process.env.NODE_ENV || "development"].connection, static: { - path: '../analytics.usa.gov/', + path: "../analytics.usa.gov/", }, }; diff --git a/ua/src/google-analytics/credential-loader.js b/ua/src/google-analytics/credential-loader.js index 1879c4ed..daf918b7 100644 --- a/ua/src/google-analytics/credential-loader.js +++ b/ua/src/google-analytics/credential-loader.js @@ -1,20 +1,22 @@ -const config = require("../config") +const config = require("../config"); -global.analyticsCredentialsIndex = 0 +global.analyticsCredentialsIndex = 0; const loadCredentials = () => { - const credentialData = JSON.parse(config.analytics_credentials) - const credentialsArray = _wrapArray(credentialData) - const index = global.analyticsCredentialsIndex++ % credentialsArray.length - return credentialsArray[index] -} + const credentialData = JSON.parse( + Buffer.from(config.analytics_credentials, "base64").toString("utf8"), + ); + const credentialsArray = _wrapArray(credentialData); + const index = global.analyticsCredentialsIndex++ % credentialsArray.length; + return credentialsArray[index]; +}; const _wrapArray = (object) => { if (Array.isArray(object)) { - return object + return object; } else { - return [object] + return [object]; } -} +}; -module.exports = { loadCredentials } +module.exports = { loadCredentials }; diff --git a/ua/src/google-analytics/query-authorizer.js b/ua/src/google-analytics/query-authorizer.js index 450b4d91..c8f18bf4 100644 --- a/ua/src/google-analytics/query-authorizer.js +++ b/ua/src/google-analytics/query-authorizer.js @@ -1,54 +1,54 @@ -const googleapis = require('googleapis') -const fs = require('fs') -const config = require('../config') -const GoogleAnalyticsCredentialLoader = require("./credential-loader") +const googleapis = require("googleapis"); +const fs = require("fs"); +const config = require("../config"); +const GoogleAnalyticsCredentialLoader = require("./credential-loader"); const authorizeQuery = (query) => { - const credentials = _getCredentials() - const email = credentials.email - const key = credentials.key - const scopes = ['https://www.googleapis.com/auth/analytics.readonly'] + const credentials = _getCredentials(); + const email = credentials.email; + const key = credentials.key; + const scopes = ["https://www.googleapis.com/auth/analytics.readonly"]; const jwt = new googleapis.Auth.JWT(email, null, key, scopes); - query = Object.assign({}, query, { auth: jwt }) + query = Object.assign({}, query, { auth: jwt }); return new Promise((resolve, reject) => { jwt.authorize((err, result) => { if (err) { - reject(err) + reject(err); } else { - resolve(query) + resolve(query); } - }) - }) -} + }); + }); +}; const _getCredentials = () => { if (config.key) { - return { key: config.key, email: config.email } + return { key: config.key, email: config.email }; } else if (config.key_file) { - return _loadCredentialsFromKeyfile(config.key_file) + return _loadCredentialsFromKeyfile(config.key_file); } else if (config.analytics_credentials) { - return GoogleAnalyticsCredentialLoader.loadCredentials() + return GoogleAnalyticsCredentialLoader.loadCredentials(); } else { - throw new Error("No key or key file specified in config") + throw new Error("No key or key file specified in config"); } -} +}; const _loadCredentialsFromKeyfile = (keyfile) => { if (!fs.existsSync(keyfile)) { - throw new Error(`No such key file: ${keyfile}`) + throw new Error(`No such key file: ${keyfile}`); } - let key = fs.readFileSync(keyfile).toString().trim() - let email = config.email + let key = fs.readFileSync(keyfile).toString().trim(); + let email = config.email; if (keyfile.match(/\.json$/)) { - const json = JSON.parse(key) - key = json.private_key - email = json.client_email + const json = JSON.parse(key); + key = json.private_key; + email = json.client_email; } - return { key, email } -} + return { key, email }; +}; -module.exports = { authorizeQuery } +module.exports = { authorizeQuery }; diff --git a/ua/src/google-analytics/query-builder.js b/ua/src/google-analytics/query-builder.js index 0a14ff82..9411d3ce 100644 --- a/ua/src/google-analytics/query-builder.js +++ b/ua/src/google-analytics/query-builder.js @@ -1,26 +1,26 @@ -const config = require('../config') +const config = require("../config"); const buildQuery = (report) => { - let query = Object.assign({}, report.query) - query = buildQueryArrays(query) + let query = Object.assign({}, report.query); + query = buildQueryArrays(query); query.samplingLevel = "HIGHER_PRECISION"; - query['max-results'] = query['max-results'] || 10000; + query["max-results"] = query["max-results"] || 10000; query.ids = config.account.ids; - return query -} + return query; +}; const buildQueryArrays = (query) => { - query = Object.assign({}, query) + query = Object.assign({}, query); if (query.dimensions) { - query.dimensions = query.dimensions.join(",") + query.dimensions = query.dimensions.join(","); } if (query.metrics) { - query.metrics = query.metrics.join(",") + query.metrics = query.metrics.join(","); } if (query.filters) { - query.filters = query.filters.join(";") + query.filters = query.filters.join(";"); } - return query -} + return query; +}; -module.exports = { buildQuery } +module.exports = { buildQuery }; diff --git a/ua/src/process-results/ga-data-processor.js b/ua/src/process-results/ga-data-processor.js index 0ef8b6fb..41e351d9 100644 --- a/ua/src/process-results/ga-data-processor.js +++ b/ua/src/process-results/ga-data-processor.js @@ -1,9 +1,9 @@ -const config = require("../config") -const ResultTotalsCalculator = require("./result-totals-calculator") +const config = require("../config"); +const ResultTotalsCalculator = require("./result-totals-calculator"); const processData = (report, responseData) => { - let { data } = responseData - let result = _initializeResult({ report, data }) + let { data } = responseData; + let result = _initializeResult({ report, data }); // If you use a filter that results in no data, you get null // back from google and need to protect against it. if (!data || !data.rows) { @@ -12,102 +12,102 @@ const processData = (report, responseData) => { // Some reports may decide to cut fields from the output. if (report.cut) { - data = _removeColumnFromData({ column: report.cut, data }) + data = _removeColumnFromData({ column: report.cut, data }); } // Remove data points that are below the threshold if one exists if (report.threshold) { - data = _filterRowsBelowThreshold({ threshold: report.threshold, data }) + data = _filterRowsBelowThreshold({ threshold: report.threshold, data }); } // Process each row - result.data = data.rows.map(row => { - return _processRow({ row, report, data }) - }) + result.data = data.rows.map((row) => { + return _processRow({ row, report, data }); + }); - result.totals = ResultTotalsCalculator.calculateTotals(result) + result.totals = ResultTotalsCalculator.calculateTotals(result); return result; -} +}; const _fieldNameForColumnIndex = ({ index, data }) => { - const name = data.columnHeaders[index].name - return _mapping[name] || name -} + const name = data.columnHeaders[index].name; + return _mapping[name] || name; +}; const _filterRowsBelowThreshold = ({ threshold, data }) => { - data = Object.assign({}, data) + data = Object.assign({}, data); - const thresholdIndex = data.columnHeaders.findIndex(header => { - return header.name === threshold.field - }) - const thresholdValue = parseInt(threshold.value) + const thresholdIndex = data.columnHeaders.findIndex((header) => { + return header.name === threshold.field; + }); + const thresholdValue = parseInt(threshold.value); - data.rows = data.rows.filter(row => { - return row[thresholdIndex] >= thresholdValue - }) + data.rows = data.rows.filter((row) => { + return row[thresholdIndex] >= thresholdValue; + }); - return data -} + return data; +}; const _formatDate = (date) => { if (date == "(other)") { - return date + return date; } - return [date.substr(0, 4), date.substr(4, 2), date.substr(6, 2)].join("-") -} + return [date.substr(0, 4), date.substr(4, 2), date.substr(6, 2)].join("-"); +}; const _initializeResult = ({ report, data }) => ({ name: report.name, sampling: { containsSampledData: data.containsSampledData, sampleSize: data.sampleSize, - sampleSpace: data.sampleSpace + sampleSpace: data.sampleSpace, }, query: ((query) => { - query = Object.assign({}, query) - delete query.ids - return query + query = Object.assign({}, query); + delete query.ids; + return query; })(data.query), meta: report.meta, data: [], totals: {}, taken_at: new Date(), -}) +}); const _processRow = ({ row, data, report }) => { - const point = {} + const point = {}; row.forEach((rowElement, index) => { - const field = _fieldNameForColumnIndex({ index, data }) - let value = rowElement + const field = _fieldNameForColumnIndex({ index, data }); + let value = rowElement; if (field === "date") { - value = _formatDate(value) + value = _formatDate(value); } - point[field] = value - }) + point[field] = value; + }); - if (config.account.hostname && !('domain' in point)) { - point.domain = config.account.hostname + if (config.account.hostname && !("domain" in point)) { + point.domain = config.account.hostname; } - return point -} + return point; +}; const _removeColumnFromData = ({ column, data }) => { - data = Object.assign(data) + data = Object.assign(data); - const columnIndex = data.columnHeaders.findIndex(header => { - return header.name === column - }) + const columnIndex = data.columnHeaders.findIndex((header) => { + return header.name === column; + }); - data.columnHeaders.splice(columnIndex, 1) - data.rows.forEach(row => { - row.splice(columnIndex, 1) - }) + data.columnHeaders.splice(columnIndex, 1); + data.rows.forEach((row) => { + row.splice(columnIndex, 1); + }); - return data -} + return data; +}; const _mapping = { "ga:date": "date", @@ -120,14 +120,14 @@ const _mapping = { "ga:operatingSystem": "os", "ga:operatingSystemVersion": "os_version", "ga:hostname": "domain", - "ga:browser": 'browser', + "ga:browser": "browser", "ga:browserVersion": "browser_version", "ga:source": "source", "ga:pagePath": "page", "ga:pageTitle": "page_title", "ga:pageviews": "visits", "ga:country": "country", - "ga:city": 'city', + "ga:city": "city", "ga:eventLabel": "event_label", "ga:totalEvents": "total_events", "ga:landingPagePath": "landing_page", @@ -146,7 +146,7 @@ const _mapping = { "rt:country": "country", "rt:city": "city", "rt:totalEvents": "total_events", - "rt:eventLabel": "event_label" -} + "rt:eventLabel": "event_label", +}; -module.exports = { processData } +module.exports = { processData }; diff --git a/ua/src/process-results/result-formatter.js b/ua/src/process-results/result-formatter.js index 924fe7f4..4db0a76c 100644 --- a/ua/src/process-results/result-formatter.js +++ b/ua/src/process-results/result-formatter.js @@ -1,29 +1,29 @@ -const csv = require("fast-csv") +const csv = require("fast-csv"); const formatResult = (result, { format = "json", slim = false } = {}) => { - result = Object.assign({}, result) + result = Object.assign({}, result); - switch(format) { + switch (format) { case "json": - return _formatJSON(result, { slim }) - break + return _formatJSON(result, { slim }); + break; case "csv": - return _formatCSV(result) - break + return _formatCSV(result); + break; default: - return Promise.reject("Unsupported format: " + format) + return Promise.reject("Unsupported format: " + format); } -} +}; const _formatJSON = (result, { slim }) => { if (slim) { - delete result.data + delete result.data; } - return Promise.resolve(JSON.stringify(result, null, 2)) -} + return Promise.resolve(JSON.stringify(result, null, 2)); +}; const _formatCSV = (result) => { - return csv.writeToString(result.data, {headers: true}) -} + return csv.writeToString(result.data, { headers: true }); +}; -module.exports = { formatResult } +module.exports = { formatResult }; diff --git a/ua/src/process-results/result-totals-calculator.js b/ua/src/process-results/result-totals-calculator.js index e137507a..b37ef4bb 100644 --- a/ua/src/process-results/result-totals-calculator.js +++ b/ua/src/process-results/result-totals-calculator.js @@ -1,16 +1,16 @@ const calculateTotals = (result) => { if (result.data.length === 0) { - return result + return result; } - let totals = {} + let totals = {}; // Sum up simple columns if ("users" in result.data[0]) { - totals.users = _sumColumn({ column: "users", result }) + totals.users = _sumColumn({ column: "users", result }); } if ("visits" in result.data[0]) { - totals.visits = _sumColumn({ column: "visits", result }) + totals.visits = _sumColumn({ column: "visits", result }); } // Sum up categories @@ -18,49 +18,49 @@ const calculateTotals = (result) => { totals.device_models = _sumVisitsByColumn({ column: "mobile_device", result, - }) + }); } if (result.name.match(/^language/)) { totals.languages = _sumVisitsByColumn({ column: "language", result, - }) + }); } if (result.name.match(/^devices/)) { totals.devices = _sumVisitsByColumn({ column: "device", result, - }) + }); } if (result.name == "screen-size") { totals.screen_resolution = _sumVisitsByColumn({ column: "screen_resolution", result, - }) + }); } if (result.name === "os") { totals.os = _sumVisitsByColumn({ column: "os", result, - }) + }); } if (result.name === "windows") { totals.os_version = _sumVisitsByColumn({ column: "os_version", result, - }) + }); } if (result.name === "browsers") { totals.browser = _sumVisitsByColumn({ column: "browser", result, - }) + }); } if (result.name === "ie") { totals.ie_version = _sumVisitsByColumn({ column: "browser_version", result, - }) + }); } // Sum up totals with 2 levels of hashes @@ -69,80 +69,80 @@ const calculateTotals = (result) => { column: "os", dimension: "browser", result, - }) + }); totals.by_browsers = _sumVisitsByCategoryWithDimension({ column: "browser", dimension: "os", result, - }) + }); } if (result.name === "windows-ie") { totals.by_windows = _sumVisitsByCategoryWithDimension({ column: "os_version", dimension: "browser_version", result, - }) + }); totals.by_ie = _sumVisitsByCategoryWithDimension({ column: "browser_version", dimension: "os_version", result, - }) + }); } if (result.name === "windows-browsers") { totals.by_windows = _sumVisitsByCategoryWithDimension({ column: "os_version", dimension: "browser", result, - }) + }); totals.by_browsers = _sumVisitsByCategoryWithDimension({ column: "browser", dimension: "os_version", result, - }) + }); } // Set the start and end date if (result.data[0].data) { // Occasionally we'll get bogus start dates if (result.date[0].date === "(other)") { - totals.start_date = result.data[1].date + totals.start_date = result.data[1].date; } else { - totals.start_date = result.data[0].date + totals.start_date = result.data[0].date; } - totals.end_date = result.data[result.data.length-1].date + totals.end_date = result.data[result.data.length - 1].date; } - return totals -} + return totals; +}; const _sumColumn = ({ result, column }) => { return result.data.reduce((total, row) => { - return parseInt(row[column]) + total - }, 0) -} + return parseInt(row[column]) + total; + }, 0); +}; const _sumVisitsByColumn = ({ result, column }) => { return result.data.reduce((categories, row) => { - const category = row[column] - const visits = parseInt(row.visits) - categories[category] = (categories[category] || 0) + visits - return categories - }, {}) -} + const category = row[column]; + const visits = parseInt(row.visits); + categories[category] = (categories[category] || 0) + visits; + return categories; + }, {}); +}; const _sumVisitsByCategoryWithDimension = ({ result, column, dimension }) => { return result.data.reduce((categories, row) => { - const parentCategory = row[column] - const childCategory = row[dimension] - const visits = parseInt(row.visits) + const parentCategory = row[column]; + const childCategory = row[dimension]; + const visits = parseInt(row.visits); - categories[parentCategory] = categories[parentCategory] || {} + categories[parentCategory] = categories[parentCategory] || {}; - const newTotal = (categories[parentCategory][childCategory] || 0) + visits - categories[parentCategory][childCategory] = newTotal + const newTotal = (categories[parentCategory][childCategory] || 0) + visits; + categories[parentCategory][childCategory] = newTotal; - return categories - }, {}) -} + return categories; + }, {}); +}; -module.exports = { calculateTotals } +module.exports = { calculateTotals }; diff --git a/ua/src/publish/disk.js b/ua/src/publish/disk.js index 653f42ad..b591b514 100644 --- a/ua/src/publish/disk.js +++ b/ua/src/publish/disk.js @@ -1,19 +1,19 @@ -const fs = require("fs") -const path = require("path") +const fs = require("fs"); +const path = require("path"); const publish = (report, results, { output, format }) => { - const filename = `${report.name}.${format}` - const filepath = path.join(output, filename) + const filename = `${report.name}.${format}`; + const filepath = path.join(output, filename); return new Promise((resolve, reject) => { - fs.writeFile(filepath, results, err => { + fs.writeFile(filepath, results, (err) => { if (err) { - reject(err) + reject(err); } else { - resolve() + resolve(); } - }) - }) -} + }); + }); +}; -module.exports = { publish } +module.exports = { publish }; diff --git a/ua/src/publish/postgres.js b/ua/src/publish/postgres.js index 17f17381..ca80e682 100644 --- a/ua/src/publish/postgres.js +++ b/ua/src/publish/postgres.js @@ -1,115 +1,120 @@ -const ANALYTICS_DATA_TABLE_NAME = "analytics_data" +const ANALYTICS_DATA_TABLE_NAME = "analytics_data"; -const knex = require("knex") -const config = require("../config") +const knex = require("knex"); +const config = require("../config"); Promise.each = async function (arr, fn) { for (const item of arr) await fn(item); -} +}; const publish = (results) => { if (results.query.dimensions.match(/ga:date/)) { - const db = knex({ client: "pg", connection: config.postgres }) - return _writeRegularResults({ db, results }).then(() => db.destroy()) + const db = knex({ client: "pg", connection: config.postgres }); + return _writeRegularResults({ db, results }).then(() => db.destroy()); } else { - return Promise.resolve() + return Promise.resolve(); } -} +}; const _convertDataAttributesToNumbers = (data) => { - const transformedData = Object.assign({}, data) + const transformedData = Object.assign({}, data); - const numbericalAttributes = ["visits", "total_events", "users"] - numbericalAttributes.forEach(attributeName => { + const numbericalAttributes = ["visits", "total_events", "users"]; + numbericalAttributes.forEach((attributeName) => { if (transformedData[attributeName]) { - transformedData[attributeName] = Number(transformedData[attributeName]) + transformedData[attributeName] = Number(transformedData[attributeName]); } - }) + }); - return transformedData -} + return transformedData; +}; const _dataForDataPoint = (dataPoint) => { - const data = _convertDataAttributesToNumbers(dataPoint) + const data = _convertDataAttributesToNumbers(dataPoint); - const date = _dateTimeForDataPoint(dataPoint) + const date = _dateTimeForDataPoint(dataPoint); - delete data.date - delete data.hour + delete data.date; + delete data.hour; return { date, data, - } -} + }; +}; const _dateTimeForDataPoint = (dataPoint) => { if (!isNaN(Date.parse(dataPoint.date))) { - return dataPoint.date + return dataPoint.date; } -} +}; const _queryForExistingRow = ({ db, row }) => { - query = db(ANALYTICS_DATA_TABLE_NAME) + query = db(ANALYTICS_DATA_TABLE_NAME); - Object.keys(row).forEach(key => { + Object.keys(row).forEach((key) => { if (row[key] === undefined) { - return + return; } else if (key === "data") { - const dataQuery = Object.assign({}, row.data) - delete dataQuery.visits - delete dataQuery.users - delete dataQuery.total_events - Object.keys(dataQuery).forEach(dataKey => { - query = query.whereRaw(`data->>'${dataKey}' = ?`, [dataQuery[dataKey]]) - }) + const dataQuery = Object.assign({}, row.data); + delete dataQuery.visits; + delete dataQuery.users; + delete dataQuery.total_events; + Object.keys(dataQuery).forEach((dataKey) => { + query = query.whereRaw(`data->>'${dataKey}' = ?`, [dataQuery[dataKey]]); + }); } else { - query = query.where({ [key]: row[key] }) + query = query.where({ [key]: row[key] }); } - }) + }); - return query.select() -} + return query.select(); +}; const _handleExistingRow = ({ db, existingRow, newRow }) => { - if (existingRow.data.visits != newRow.data.visits || - existingRow.data.users != newRow.data.users || - existingRow.data.total_events != newRow.data.total_events + if ( + existingRow.data.visits != newRow.data.visits || + existingRow.data.users != newRow.data.users || + existingRow.data.total_events != newRow.data.total_events ) { - return db(ANALYTICS_DATA_TABLE_NAME).where({ id: existingRow.id }).update(newRow) + return db(ANALYTICS_DATA_TABLE_NAME) + .where({ id: existingRow.id }) + .update(newRow); } -} +}; const _rowForDataPoint = ({ results, dataPoint }) => { - const row = _dataForDataPoint(dataPoint) - row.report_name = results.name - row.report_agency = results.agency - return row -} + const row = _dataForDataPoint(dataPoint); + row.report_name = results.name; + row.report_agency = results.agency; + return row; +}; const _writeRegularResults = ({ db, results }) => { - const rows = results.data.map(dataPoint => { - return _rowForDataPoint({ results, dataPoint }) - }) + const rows = results.data.map((dataPoint) => { + return _rowForDataPoint({ results, dataPoint }); + }); - const rowsToInsert = [] - return Promise.each(rows, row => { - return _queryForExistingRow({ db, row }).then(results => { + const rowsToInsert = []; + return Promise.each(rows, (row) => { + return _queryForExistingRow({ db, row }).then((results) => { if (row.date === undefined) { - return + return; } else if (results.length === 0) { - rowsToInsert.push(row) + rowsToInsert.push(row); } else if (results.length === 1) { - return _handleExistingRow({ db, existingRow: results[0], newRow: row }) + return _handleExistingRow({ db, existingRow: results[0], newRow: row }); } - }) - }).then(() => { - if(rowsToInsert.length > 0) { - return db(ANALYTICS_DATA_TABLE_NAME).insert(rowsToInsert) - } - }).then(() => { - return db.destroy() + }); }) -} + .then(() => { + if (rowsToInsert.length > 0) { + return db(ANALYTICS_DATA_TABLE_NAME).insert(rowsToInsert); + } + }) + .then(() => { + return db.destroy(); + }); +}; -module.exports = { publish, ANALYTICS_DATA_TABLE_NAME } +module.exports = { publish, ANALYTICS_DATA_TABLE_NAME }; diff --git a/ua/test/analytics.test.js b/ua/test/analytics.test.js index b54d3d29..54830fb3 100644 --- a/ua/test/analytics.test.js +++ b/ua/test/analytics.test.js @@ -1,60 +1,66 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); describe("UA Analytics", () => { - let Analytics - let GoogleAnalyticsClient - let GoogleAnalyticsDataProcessor + let Analytics; + let GoogleAnalyticsClient; + let GoogleAnalyticsDataProcessor; beforeEach(() => { - GoogleAnalyticsClient = {} - GoogleAnalyticsDataProcessor = {} + GoogleAnalyticsClient = {}; + GoogleAnalyticsDataProcessor = {}; Analytics = proxyquire("../src/analytics", { "./google-analytics/client": GoogleAnalyticsClient, "./process-results/ga-data-processor": GoogleAnalyticsDataProcessor, - }) - }) + }); + }); describe(".query(report)", () => { - it("should resolve with formatted google analytics data for the given reports", done => { - const report = { name: "Report Name" } - const data = { rows: [1, 2, 3] } - const processedData = { data: [1, 2, 3] } + it("should resolve with formatted google analytics data for the given reports", (done) => { + const report = { name: "Report Name" }; + const data = { rows: [1, 2, 3] }; + const processedData = { data: [1, 2, 3] }; - let fetchDataCalled = false - let processedDataCalled = false + let fetchDataCalled = false; + let processedDataCalled = false; GoogleAnalyticsClient.fetchData = (reportInput) => { - fetchDataCalled = true - expect(reportInput).to.equal(report) - return Promise.resolve(data) - } + fetchDataCalled = true; + expect(reportInput).to.equal(report); + return Promise.resolve(data); + }; GoogleAnalyticsDataProcessor.processData = (reportInput, dataInput) => { - processedDataCalled = true - expect(reportInput).to.equal(report) - expect(dataInput).to.equal(data) - return Promise.resolve(processedData) - } - - Analytics.query(report).then(results => { - expect(results).to.equal(processedData) - expect(fetchDataCalled).to.be.true - expect(processedDataCalled).to.be.true - done() - }).catch(done) - }) - - it("should reject if no report is provided", done => { - Analytics.query().catch(err => { - expect(err.message).to.equal("Analytics.query missing required argument `report`") - done() - }).catch(done) - }) - }) + processedDataCalled = true; + expect(reportInput).to.equal(report); + expect(dataInput).to.equal(data); + return Promise.resolve(processedData); + }; + + Analytics.query(report) + .then((results) => { + expect(results).to.equal(processedData); + expect(fetchDataCalled).to.be.true; + expect(processedDataCalled).to.be.true; + done(); + }) + .catch(done); + }); + + it("should reject if no report is provided", (done) => { + Analytics.query() + .catch((err) => { + expect(err.message).to.equal( + "Analytics.query missing required argument `report`", + ); + done(); + }) + .catch(done); + }); + }); describe(".reports", () => { it("should load reports", () => { - expect(Analytics.reports).to.be.an("array") - }) - }) -}) + expect(Analytics.reports).to.be.an("array"); + }); + }); +}); diff --git a/ua/test/google-analytics/credential-loader.test.js b/ua/test/google-analytics/credential-loader.test.js index 3e99884a..37b326c9 100644 --- a/ua/test/google-analytics/credential-loader.test.js +++ b/ua/test/google-analytics/credential-loader.test.js @@ -1,34 +1,41 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); -proxyquire.noCallThru() +proxyquire.noCallThru(); -const config = {} +const config = {}; -const GoogleAnalyticsCredentialLoader = proxyquire("../../src/google-analytics/credential-loader", { - "../config": config, -}) +const GoogleAnalyticsCredentialLoader = proxyquire( + "../../src/google-analytics/credential-loader", + { + "../config": config, + }, +); describe("UA GoogleAnalyticsCredentialLoader", () => { describe(".loadCredentials()", () => { beforeEach(() => { - config.analytics_credentials = undefined - global.analyticsCredentialsIndex = 0 - }) + config.analytics_credentials = undefined; + global.analyticsCredentialsIndex = 0; + }); it("should return the credentials if the credentials are an object", () => { - config.analytics_credentials = `{ + config.analytics_credentials = Buffer.from( + `{ "email": "email@example.com", "key": "this-is-a-secret" - }` + }`, + "utf8", + ).toString("base64"); - const creds = GoogleAnalyticsCredentialLoader.loadCredentials() - expect(creds.email).to.equal("email@example.com") - expect(creds.key).to.equal("this-is-a-secret") - }) + const creds = GoogleAnalyticsCredentialLoader.loadCredentials(); + expect(creds.email).to.equal("email@example.com"); + expect(creds.key).to.equal("this-is-a-secret"); + }); it("should return successive credentials if the credentials are an array", () => { - config.analytics_credentials = `[ + config.analytics_credentials = Buffer.from( + `[ { "email": "email_1@example.com", "key": "this-is-a-secret-1" @@ -37,18 +44,20 @@ describe("UA GoogleAnalyticsCredentialLoader", () => { "email": "email_2@example.com", "key": "this-is-a-secret-2" } - ]` - - const firstCreds = GoogleAnalyticsCredentialLoader.loadCredentials() - const secondCreds = GoogleAnalyticsCredentialLoader.loadCredentials() - const thirdCreds = GoogleAnalyticsCredentialLoader.loadCredentials() - - expect(firstCreds.email).to.equal("email_1@example.com") - expect(firstCreds.key).to.equal("this-is-a-secret-1") - expect(secondCreds.email).to.equal("email_2@example.com") - expect(secondCreds.key).to.equal("this-is-a-secret-2") - expect(thirdCreds.email).to.equal("email_1@example.com") - expect(thirdCreds.key).to.equal("this-is-a-secret-1") - }) - }) -}) + ]`, + "utf8", + ).toString("base64"); + + const firstCreds = GoogleAnalyticsCredentialLoader.loadCredentials(); + const secondCreds = GoogleAnalyticsCredentialLoader.loadCredentials(); + const thirdCreds = GoogleAnalyticsCredentialLoader.loadCredentials(); + + expect(firstCreds.email).to.equal("email_1@example.com"); + expect(firstCreds.key).to.equal("this-is-a-secret-1"); + expect(secondCreds.email).to.equal("email_2@example.com"); + expect(secondCreds.key).to.equal("this-is-a-secret-2"); + expect(thirdCreds.email).to.equal("email_1@example.com"); + expect(thirdCreds.key).to.equal("this-is-a-secret-1"); + }); + }); +}); diff --git a/ua/test/google-analytics/query-authorizer.test.js b/ua/test/google-analytics/query-authorizer.test.js index 12dd756d..a59d4208 100644 --- a/ua/test/google-analytics/query-authorizer.test.js +++ b/ua/test/google-analytics/query-authorizer.test.js @@ -1,125 +1,154 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") -const googleAPIsMock = require("../support/mocks/googleapis-auth") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const googleAPIsMock = require("../support/mocks/googleapis-auth"); -proxyquire.noCallThru() +proxyquire.noCallThru(); -const config = {} -const googleapis = {} +const config = {}; +const googleapis = {}; const GoogleAnalyticsCredentialLoader = { loadCredentials: () => ({ email: "next_email@example.com", key: "Shhh, this is the next secret", - }) -} - -const GoogleAnalyticsQueryAuthorizer = proxyquire("../../src/google-analytics/query-authorizer", { - "../config": config, - "./credential-loader": GoogleAnalyticsCredentialLoader, - googleapis, -}) + }), +}; + +const GoogleAnalyticsQueryAuthorizer = proxyquire( + "../../src/google-analytics/query-authorizer", + { + "../config": config, + "./credential-loader": GoogleAnalyticsCredentialLoader, + googleapis, + }, +); describe("UA GoogleAnalyticsQueryAuthorizer", () => { describe(".authorizeQuery(query)", () => { beforeEach(() => { - Object.assign(googleapis, googleAPIsMock()) - config.email = "hello@example.com" - config.key = "123abc" - config.key_file = undefined - }) + Object.assign(googleapis, googleAPIsMock()); + config.email = "hello@example.com"; + config.key = "123abc"; + config.key_file = undefined; + }); - it("should resolve a query with the auth prop set to an authorized JWT", done => { + it("should resolve a query with the auth prop set to an authorized JWT", (done) => { query = { - "abc": 123 - } - - GoogleAnalyticsQueryAuthorizer.authorizeQuery(query).then(query => { - expect(query.abc).to.equal(123) - expect(query.auth).to.not.be.undefined - expect(query.auth).to.be.an.instanceof(googleapis.Auth.JWT) - done() - }).catch(done) - }) - - it("should create a JWT with the key and email in the config if one exists", done => { - config.email = "test@example.com" - config.key = "Shh, this is a secret" - - GoogleAnalyticsQueryAuthorizer.authorizeQuery({}).then(query => { - expect(query.auth.initArguments[0]).to.equal("test@example.com") - expect(query.auth.initArguments[2]).to.equal("Shh, this is a secret") - done() - }).catch(done) - }) - - it("should create a JWT from the keyfile and the email in the config if one exists", done => { - config.email = "test@example.com" - config.key = undefined - config.key_file = "./test/support/fixtures/secret_key.pem" - - GoogleAnalyticsQueryAuthorizer.authorizeQuery({}).then(query => { - expect(query.auth.initArguments[0]).to.equal("test@example.com") - expect(query.auth.initArguments[2]).to.equal("pem-key-file-not-actually-a-secret-key") - done() - }).catch(done) - }) - - it("should create a JWT from the JSON keyfile in the config if one exists", done => { - config.key = undefined - config.key_file = "./test/support/fixtures/secret_key.json" - - GoogleAnalyticsQueryAuthorizer.authorizeQuery({}).then(query => { - expect(query.auth.initArguments[0]).to.equal("json_test_email@example.com") - expect(query.auth.initArguments[2]).to.equal("json-key-file-not-actually-a-secret-key") - done() - }).catch(done) - }) - - it("should create a JWT with credentials from calling GoogleAnalyticsCredentialLoader for analytics credentials in the config", done => { - config.key = undefined - config.analytics_credentials = "[{}]" // overriden by proxyquire - - GoogleAnalyticsQueryAuthorizer.authorizeQuery({}).then(query => { - expect(query.auth.initArguments[0]).to.equal("next_email@example.com") - expect(query.auth.initArguments[2]).to.equal("Shhh, this is the next secret") - done() - }).catch(done) - }) - - it("should create a JWT with the proper scopes", done => { - GoogleAnalyticsQueryAuthorizer.authorizeQuery({}).then(query => { - expect(query.auth.initArguments[3]).to.deep.equal([ - "https://www.googleapis.com/auth/analytics.readonly" - ]) - done() - }).catch(done) - }) - - it("should authorize the JWT and resolve if it is valid", done => { - let jwtAuthorized = false + abc: 123, + }; + + GoogleAnalyticsQueryAuthorizer.authorizeQuery(query) + .then((query) => { + expect(query.abc).to.equal(123); + expect(query.auth).to.not.be.undefined; + expect(query.auth).to.be.an.instanceof(googleapis.Auth.JWT); + done(); + }) + .catch(done); + }); + + it("should create a JWT with the key and email in the config if one exists", (done) => { + config.email = "test@example.com"; + config.key = "Shh, this is a secret"; + + GoogleAnalyticsQueryAuthorizer.authorizeQuery({}) + .then((query) => { + expect(query.auth.initArguments[0]).to.equal("test@example.com"); + expect(query.auth.initArguments[2]).to.equal("Shh, this is a secret"); + done(); + }) + .catch(done); + }); + + it("should create a JWT from the keyfile and the email in the config if one exists", (done) => { + config.email = "test@example.com"; + config.key = undefined; + config.key_file = "./test/support/fixtures/secret_key.pem"; + + GoogleAnalyticsQueryAuthorizer.authorizeQuery({}) + .then((query) => { + expect(query.auth.initArguments[0]).to.equal("test@example.com"); + expect(query.auth.initArguments[2]).to.equal( + "pem-key-file-not-actually-a-secret-key", + ); + done(); + }) + .catch(done); + }); + + it("should create a JWT from the JSON keyfile in the config if one exists", (done) => { + config.key = undefined; + config.key_file = "./test/support/fixtures/secret_key.json"; + + GoogleAnalyticsQueryAuthorizer.authorizeQuery({}) + .then((query) => { + expect(query.auth.initArguments[0]).to.equal( + "json_test_email@example.com", + ); + expect(query.auth.initArguments[2]).to.equal( + "json-key-file-not-actually-a-secret-key", + ); + done(); + }) + .catch(done); + }); + + it("should create a JWT with credentials from calling GoogleAnalyticsCredentialLoader for analytics credentials in the config", (done) => { + config.key = undefined; + config.analytics_credentials = "[{}]"; // overriden by proxyquire + + GoogleAnalyticsQueryAuthorizer.authorizeQuery({}) + .then((query) => { + expect(query.auth.initArguments[0]).to.equal( + "next_email@example.com", + ); + expect(query.auth.initArguments[2]).to.equal( + "Shhh, this is the next secret", + ); + done(); + }) + .catch(done); + }); + + it("should create a JWT with the proper scopes", (done) => { + GoogleAnalyticsQueryAuthorizer.authorizeQuery({}) + .then((query) => { + expect(query.auth.initArguments[3]).to.deep.equal([ + "https://www.googleapis.com/auth/analytics.readonly", + ]); + done(); + }) + .catch(done); + }); + + it("should authorize the JWT and resolve if it is valid", (done) => { + let jwtAuthorized = false; googleapis.Auth.JWT.prototype.authorize = (callback) => { - jwtAuthorized = true - callback(null, {}) - } - - GoogleAnalyticsQueryAuthorizer.authorizeQuery({}).then(query => { - expect(jwtAuthorized).to.equal(true) - done() - }).catch(done) - }) - - it("should authorize the JWT and reject if it is invalid", done => { - let jwtAuthorized = false + jwtAuthorized = true; + callback(null, {}); + }; + + GoogleAnalyticsQueryAuthorizer.authorizeQuery({}) + .then((query) => { + expect(jwtAuthorized).to.equal(true); + done(); + }) + .catch(done); + }); + + it("should authorize the JWT and reject if it is invalid", (done) => { + let jwtAuthorized = false; googleapis.Auth.JWT.prototype.authorize = (callback) => { - jwtAuthorized = true - callback(new Error("Failed to authorize")) - } - - GoogleAnalyticsQueryAuthorizer.authorizeQuery({}).catch(err => { - expect(jwtAuthorized).to.equal(true) - expect(err.message).to.equal("Failed to authorize") - done() - }).catch(done) - }) - }) -}) + jwtAuthorized = true; + callback(new Error("Failed to authorize")); + }; + + GoogleAnalyticsQueryAuthorizer.authorizeQuery({}) + .catch((err) => { + expect(jwtAuthorized).to.equal(true); + expect(err.message).to.equal("Failed to authorize"); + done(); + }) + .catch(done); + }); + }); +}); diff --git a/ua/test/google-analytics/query-builder.test.js b/ua/test/google-analytics/query-builder.test.js index 2f0e9ad1..49719fdb 100644 --- a/ua/test/google-analytics/query-builder.test.js +++ b/ua/test/google-analytics/query-builder.test.js @@ -1,84 +1,87 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") -const reportFixture = require("../support/fixtures/report") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const reportFixture = require("../support/fixtures/report"); -proxyquire.noCallThru() +proxyquire.noCallThru(); -const config = {} +const config = {}; -const GoogleAnalyticsQueryBuilder = proxyquire("../../src/google-analytics/query-builder", { - "../config": config, -}) +const GoogleAnalyticsQueryBuilder = proxyquire( + "../../src/google-analytics/query-builder", + { + "../config": config, + }, +); describe("UA GoogleAnalyticsQueryBuilder", () => { describe(".buildQuery(report)", () => { - let report + let report; beforeEach(() => { - report = Object.assign({}, reportFixture) + report = Object.assign({}, reportFixture); config.account = { ids: "ga:123456", - } - }) + }; + }); it("should set the properties from the query object on the report", () => { report.query = { a: "123abc", b: "456def", - } + }; - const query = GoogleAnalyticsQueryBuilder.buildQuery(report) - expect(query.a).to.equal("123abc") - expect(query.b).to.equal("456def") - }) + const query = GoogleAnalyticsQueryBuilder.buildQuery(report); + expect(query.a).to.equal("123abc"); + expect(query.b).to.equal("456def"); + }); it("should convert dimensions and metrics arrays into comma separated strings", () => { - report.query.dimensions = ["ga:date", "ga:hour"] - report.query.metrics = ["ga:sessions"] + report.query.dimensions = ["ga:date", "ga:hour"]; + report.query.metrics = ["ga:sessions"]; - const query = GoogleAnalyticsQueryBuilder.buildQuery(report) - expect(query.dimensions).to.equal("ga:date,ga:hour") - expect(query.metrics).to.equal("ga:sessions") - }) + const query = GoogleAnalyticsQueryBuilder.buildQuery(report); + expect(query.dimensions).to.equal("ga:date,ga:hour"); + expect(query.metrics).to.equal("ga:sessions"); + }); it("should convert filters array into a semicolon separated string", () => { report.query.filters = [ "ga:browser==Internet Explorer", "ga:operatingSystem==Windows", - ] + ]; - const query = GoogleAnalyticsQueryBuilder.buildQuery(report) + const query = GoogleAnalyticsQueryBuilder.buildQuery(report); expect(query.filters).to.equal( - "ga:browser==Internet Explorer;ga:operatingSystem==Windows" - ) - }) + "ga:browser==Internet Explorer;ga:operatingSystem==Windows", + ); + }); it("should set the samplingLevel to HIGHER_PRECISION", () => { - report.query.samplingLevel = undefined + report.query.samplingLevel = undefined; - const query = GoogleAnalyticsQueryBuilder.buildQuery(report) - expect(query.samplingLevel).to.equal("HIGHER_PRECISION") - }) + const query = GoogleAnalyticsQueryBuilder.buildQuery(report); + expect(query.samplingLevel).to.equal("HIGHER_PRECISION"); + }); it("should set max-results if it is set on the report", () => { - report.query["max-results"] = 3 + report.query["max-results"] = 3; - const query = GoogleAnalyticsQueryBuilder.buildQuery(report) - expect(query["max-results"]).to.equal(3) - }) + const query = GoogleAnalyticsQueryBuilder.buildQuery(report); + expect(query["max-results"]).to.equal(3); + }); it("should set max-results to 10000 if it is unset on the report", () => { - report.query["max-results"] = undefined + report.query["max-results"] = undefined; - const query = GoogleAnalyticsQueryBuilder.buildQuery(report) - expect(query["max-results"]).to.equal(10000) - }) + const query = GoogleAnalyticsQueryBuilder.buildQuery(report); + expect(query["max-results"]).to.equal(10000); + }); it("should set the ids to the account ids specified by the config", () => { - config.account.ids = "ga:abc123" + config.account.ids = "ga:abc123"; - const query = GoogleAnalyticsQueryBuilder.buildQuery(report) - expect(query.ids).to.equal("ga:abc123") - }) - }) -}) + const query = GoogleAnalyticsQueryBuilder.buildQuery(report); + expect(query.ids).to.equal("ga:abc123"); + }); + }); +}); diff --git a/ua/test/index.test.js b/ua/test/index.test.js index 1a1b5793..9c3608b6 100644 --- a/ua/test/index.test.js +++ b/ua/test/index.test.js @@ -1,34 +1,35 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") -const resultFixture = require("./support/fixtures/results") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const resultFixture = require("./support/fixtures/results"); describe("UA main", () => { describe(".run(options)", () => { - const consoleLogOriginal = console.log + const consoleLogOriginal = console.log; after(() => { - console.log = consoleLogOriginal - }) + console.log = consoleLogOriginal; + }); - const config = {} + const config = {}; - let Analytics - let DiskPublisher - let PostgresPublisher - let ResultFormatter - let result - let main + let Analytics; + let DiskPublisher; + let PostgresPublisher; + let ResultFormatter; + let result; + let main; beforeEach(() => { - result = {} + result = {}; Analytics = { reports: [{ name: "a" }, { name: "b" }, { name: "c" }], - query: (report) => Promise.resolve(Object.assign(result, { name: report.name })), - } - DiskPublisher = {} - PostgresPublisher = {} + query: (report) => + Promise.resolve(Object.assign(result, { name: report.name })), + }; + DiskPublisher = {}; + PostgresPublisher = {}; ResultFormatter = { - formatResult: (result) => Promise.resolve(JSON.stringify(result)) - } + formatResult: (result) => Promise.resolve(JSON.stringify(result)), + }; main = proxyquire("../index.js", { "./src/config": config, @@ -36,190 +37,220 @@ describe("UA main", () => { "./src/publish/disk": DiskPublisher, "./src/publish/postgres": PostgresPublisher, "./src/process-results/result-formatter": ResultFormatter, - }) - }) + }); + }); - it("should query for every single report", done => { - const queriedReportNames = [] + it("should query for every single report", (done) => { + const queriedReportNames = []; Analytics.query = (report) => { - queriedReportNames.push(report.name) - return Promise.resolve(result) - } - - main.run().then(() => { - expect(queriedReportNames).to.include.members(["a", "b", "c"]) - done() - }).catch(done) - }) - - it("should log formatted results", done => { - ResultFormatter.formatResult = () => Promise.resolve("I'm the results!") - - let consoleLogCalled = false + queriedReportNames.push(report.name); + return Promise.resolve(result); + }; + + main + .run() + .then(() => { + expect(queriedReportNames).to.include.members(["a", "b", "c"]); + done(); + }) + .catch(done); + }); + + it("should log formatted results", (done) => { + ResultFormatter.formatResult = () => Promise.resolve("I'm the results!"); + + let consoleLogCalled = false; console.log = function (output) { if (output === "I'm the results!") { - consoleLogCalled = true + consoleLogCalled = true; } else { - consoleLogOriginal.apply(this, arguments) + consoleLogOriginal.apply(this, arguments); } - } - - main.run().then(() => { - console.log = consoleLogOriginal - expect(consoleLogCalled).to.be.true - done() - }).catch(err => { - console.log = consoleLogOriginal - done(err) - }) - }) - - it("should format the results with the format set to JSON", done => { - let formatResultCalled = false + }; + + main + .run() + .then(() => { + console.log = consoleLogOriginal; + expect(consoleLogCalled).to.be.true; + done(); + }) + .catch((err) => { + console.log = consoleLogOriginal; + done(err); + }); + }); + + it("should format the results with the format set to JSON", (done) => { + let formatResultCalled = false; ResultFormatter.formatResult = (result, options) => { - expect(options.format).to.equal("json") - formatResultCalled = true - return Promise.resolve("") - } - - main.run().then(() => { - expect(formatResultCalled).to.be.true - done() - }).catch(done) - }) + expect(options.format).to.equal("json"); + formatResultCalled = true; + return Promise.resolve(""); + }; + + main + .run() + .then(() => { + expect(formatResultCalled).to.be.true; + done(); + }) + .catch(done); + }); context("with --output option", () => { - it("should write the results to the given path folder", done => { - ResultFormatter.formatResult = () => Promise.resolve("I'm the result") + it("should write the results to the given path folder", (done) => { + ResultFormatter.formatResult = () => Promise.resolve("I'm the result"); - const writtenReportNames = [] + const writtenReportNames = []; DiskPublisher.publish = (report, formattedResult, options) => { - expect(options.format).to.equal("json") - expect(options.output).to.equal("path/to/output") - expect(formattedResult).to.equal("I'm the result") - writtenReportNames.push(report.name) - } - - main.run({ output: "path/to/output" }).then(() => { - expect(writtenReportNames).to.include.members(["a", "b", "c"]) - done() - }).catch(done) - }) - }) + expect(options.format).to.equal("json"); + expect(options.output).to.equal("path/to/output"); + expect(formattedResult).to.equal("I'm the result"); + writtenReportNames.push(report.name); + }; + + main + .run({ output: "path/to/output" }) + .then(() => { + expect(writtenReportNames).to.include.members(["a", "b", "c"]); + done(); + }) + .catch(done); + }); + }); context("with --write-to-database option", () => { - it("should write the results to postgres", done => { - result = { data: "I am the result" } + it("should write the results to postgres", (done) => { + result = { data: "I am the result" }; - let publishCalled = false + let publishCalled = false; PostgresPublisher.publish = (resultToPublish) => { - expect(resultToPublish).to.deep.equal(result) - publishCalled = true - return Promise.resolve() - } - - main.run({ ["write-to-database"]: true }).then(() => { - expect(publishCalled).to.be.true - done() - }).catch(done) - }) - - it("should not write the results to postgres if the report is realtime", done => { - let publishCalled = false + expect(resultToPublish).to.deep.equal(result); + publishCalled = true; + return Promise.resolve(); + }; + + main + .run({ ["write-to-database"]: true }) + .then(() => { + expect(publishCalled).to.be.true; + done(); + }) + .catch(done); + }); + + it("should not write the results to postgres if the report is realtime", (done) => { + let publishCalled = false; PostgresPublisher.publish = () => { - publishCalled = true - return Promise.resolve() - } - - main.run({ ["write-to-database"]: false }).then(() => { - expect(publishCalled).to.be.false - done() - }).catch(done) - }) - }) + publishCalled = true; + return Promise.resolve(); + }; + + main + .run({ ["write-to-database"]: false }) + .then(() => { + expect(publishCalled).to.be.false; + done(); + }) + .catch(done); + }); + }); context("with --only option", () => { - it("should only query the given report", done => { - const queriedReportNames = [] + it("should only query the given report", (done) => { + const queriedReportNames = []; Analytics.query = (report) => { - queriedReportNames.push(report.name) - return Promise.resolve(result) - } - - main.run({ only: "a" }).then(() => { - expect(queriedReportNames).to.include("a") - expect(queriedReportNames).not.to.include.members(["b", "c"]) - done() - }).catch(done) - }) - }) + queriedReportNames.push(report.name); + return Promise.resolve(result); + }; + + main + .run({ only: "a" }) + .then(() => { + expect(queriedReportNames).to.include("a"); + expect(queriedReportNames).not.to.include.members(["b", "c"]); + done(); + }) + .catch(done); + }); + }); context("with --slim option", () => { - it("should format the results with the slim option for slim reports", done => { + it("should format the results with the slim option for slim reports", (done) => { Analytics.reports = [ { name: "a", slim: false }, { name: "b", slim: true }, { name: "c", slim: false }, - ] + ]; - const formattedSlimReportNames = [] - const formattedRegularReportNames = [] + const formattedSlimReportNames = []; + const formattedRegularReportNames = []; ResultFormatter.formatResult = (result, options) => { if (options.slim === true) { - formattedSlimReportNames.push(result.name) + formattedSlimReportNames.push(result.name); } else { - formattedRegularReportNames.push(result.name) + formattedRegularReportNames.push(result.name); } - return Promise.resolve("") - } - - main.run({ slim: true }).then(() => { - expect(formattedSlimReportNames).to.include.members(["b"]) - expect(formattedRegularReportNames).to.include.members(["a", "c"]) - done() - }).catch(done) - }) - }) + return Promise.resolve(""); + }; + + main + .run({ slim: true }) + .then(() => { + expect(formattedSlimReportNames).to.include.members(["b"]); + expect(formattedRegularReportNames).to.include.members(["a", "c"]); + done(); + }) + .catch(done); + }); + }); context("with --csv option", () => { - it("should format the reports with the format set to csv", done => { - const formattedReportNames = [] + it("should format the reports with the format set to csv", (done) => { + const formattedReportNames = []; ResultFormatter.formatResult = (result, options) => { - expect(options.format).to.equal("csv") - formattedReportNames.push(result.name) - return Promise.resolve("") - } - - main.run({ csv: true }).then(() => { - expect(formattedReportNames).to.include.members(["a", "b", "c"]) - done() - }).catch(done) - }) - }) + expect(options.format).to.equal("csv"); + formattedReportNames.push(result.name); + return Promise.resolve(""); + }; + + main + .run({ csv: true }) + .then(() => { + expect(formattedReportNames).to.include.members(["a", "b", "c"]); + done(); + }) + .catch(done); + }); + }); context("with --frequency option", () => { - it("should only query reports with the given frequency", done => { + it("should only query reports with the given frequency", (done) => { Analytics.reports = [ { name: "a", frequency: "daily" }, { name: "b", frequency: "hourly" }, { name: "c", frequency: "daily" }, - ] + ]; - const queriedReportNames = [] + const queriedReportNames = []; Analytics.query = (report) => { - queriedReportNames.push(report.name) - return Promise.resolve(result) - } - - main.run({ frequency: "daily" }).then(() => { - expect(queriedReportNames).to.include.members(["a", "c"]) - expect(queriedReportNames).not.to.include.members(["b"]) - done() - }).catch(done) - }) - }) - }) -}) + queriedReportNames.push(report.name); + return Promise.resolve(result); + }; + + main + .run({ frequency: "daily" }) + .then(() => { + expect(queriedReportNames).to.include.members(["a", "c"]); + expect(queriedReportNames).not.to.include.members(["b"]); + done(); + }) + .catch(done); + }); + }); + }); +}); diff --git a/ua/test/process-results/ga-data-processor.test.js b/ua/test/process-results/ga-data-processor.test.js index 8dbab64f..5d80ab3f 100644 --- a/ua/test/process-results/ga-data-processor.test.js +++ b/ua/test/process-results/ga-data-processor.test.js @@ -1,16 +1,19 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") -const reportFixture = require("../support/fixtures/report") -const dataFixture = require("../support/fixtures/data") -const dataWithHostnameFixture = require("../support/fixtures/data_with_hostname") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const reportFixture = require("../support/fixtures/report"); +const dataFixture = require("../support/fixtures/data"); +const dataWithHostnameFixture = require("../support/fixtures/data_with_hostname"); -proxyquire.noCallThru() +proxyquire.noCallThru(); -const config = {} +const config = {}; -const GoogleAnalyticsDataProcessor = proxyquire("../../src/process-results/ga-data-processor", { - "../config": config, -}) +const GoogleAnalyticsDataProcessor = proxyquire( + "../../src/process-results/ga-data-processor", + { + "../config": config, + }, +); describe("UA GoogleAnalyticsDataProcessor", () => { describe(".processData(report, data)", () => { @@ -19,109 +22,154 @@ describe("UA GoogleAnalyticsDataProcessor", () => { let responseData; beforeEach(() => { - report = Object.assign({}, reportFixture) - data = Object.assign({}, dataFixture) + report = Object.assign({}, reportFixture); + data = Object.assign({}, dataFixture); responseData = { data: data }; config.account = { - hostname: "" - } - }) + hostname: "", + }; + }); it("should return results with the correct props", () => { - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) - expect(result.name).to.be.a("string") - expect(result.query).be.an("object") - expect(result.meta).be.an("object") - expect(result.data).be.an("array") - expect(result.totals).be.an("object") - expect(result.totals).be.an("object") - expect(result.taken_at).be.a("date") - }) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); + expect(result.name).to.be.a("string"); + expect(result.query).be.an("object"); + expect(result.meta).be.an("object"); + expect(result.data).be.an("array"); + expect(result.totals).be.an("object"); + expect(result.totals).be.an("object"); + expect(result.taken_at).be.a("date"); + }); it("should return results with an empty data array if data is undefined or has no rows", () => { - data.rows = [] - expect(GoogleAnalyticsDataProcessor.processData(report, responseData).data).to.be.empty - data.rows = undefined - expect(GoogleAnalyticsDataProcessor.processData(report, responseData).data).to.be.empty - }) + data.rows = []; + expect( + GoogleAnalyticsDataProcessor.processData(report, responseData).data, + ).to.be.empty; + data.rows = undefined; + expect( + GoogleAnalyticsDataProcessor.processData(report, responseData).data, + ).to.be.empty; + }); it("should delete the query ids for the GA response", () => { - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) - expect(result.query).to.not.have.property("ids") - }) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); + expect(result.query).to.not.have.property("ids"); + }); it("should map data from GA keys to DAP keys", () => { data.columnHeaders = [ - { name: "ga:date" }, { name: "ga:browser" }, { name: "ga:city" } - ] - data.rows = [["20170130", "chrome", "Baton Rouge, La"]] - - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) - expect(Object.keys(result.data[0])).to.deep.equal(["date", "browser", "city"]) - }) + { name: "ga:date" }, + { name: "ga:browser" }, + { name: "ga:city" }, + ]; + data.rows = [["20170130", "chrome", "Baton Rouge, La"]]; + + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); + expect(Object.keys(result.data[0])).to.deep.equal([ + "date", + "browser", + "city", + ]); + }); it("should format dates", () => { - data.columnHeaders = [{ name: 'ga:date' }] - data.rows = [["20170130"]] + data.columnHeaders = [{ name: "ga:date" }]; + data.rows = [["20170130"]]; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) - expect(result.data[0].date).to.equal("2017-01-30") - }) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); + expect(result.data[0].date).to.equal("2017-01-30"); + }); it("should filter rows that don't meet the threshold if a threshold is provided", () => { report.threshold = { field: "unmapped_column", value: "10", - } - data.columnHeaders = [{ name: "unmapped_column" }] - data.rows = [[20], [5], [15]] - - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) - expect(result.data).to.have.length(2) - expect(result.data.map(row => row.unmapped_column)).to.deep.equal([20, 15]) - }) + }; + data.columnHeaders = [{ name: "unmapped_column" }]; + data.rows = [[20], [5], [15]]; + + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); + expect(result.data).to.have.length(2); + expect(result.data.map((row) => row.unmapped_column)).to.deep.equal([ + 20, 15, + ]); + }); it("should remove dimensions that are specified by the cut prop", () => { - report.cut = "unmapped_column" - data.columnHeaders = [{ name: "ga:hostname" }, { name: "unmapped_column" }] - data.rows = [["www.example.gov", 10000000]] - - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) - expect(result.data[0].unmapped_column).to.be.undefined - }) + report.cut = "unmapped_column"; + data.columnHeaders = [ + { name: "ga:hostname" }, + { name: "unmapped_column" }, + ]; + data.rows = [["www.example.gov", 10000000]]; + + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); + expect(result.data[0].unmapped_column).to.be.undefined; + }); it("should add a hostname to realtime data if a hostname is specified by the config", () => { - report.realtime = true - config.account.hostname = "www.example.gov" + report.realtime = true; + config.account.hostname = "www.example.gov"; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) - expect(result.data[0].domain).to.equal("www.example.gov") - }) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); + expect(result.data[0].domain).to.equal("www.example.gov"); + }); it("should not overwrite the domain with a hostname from the config", () => { - let dataWithHostname - dataWithHostname = Object.assign({}, dataWithHostnameFixture) + let dataWithHostname; + dataWithHostname = Object.assign({}, dataWithHostnameFixture); responseData = { data: dataWithHostname }; - report.realtime = true - config.account.hostname = "www.example.gov" + report.realtime = true; + config.account.hostname = "www.example.gov"; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) - expect(result.data[0].domain).to.equal("www.example0.com") - }) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); + expect(result.data[0].domain).to.equal("www.example0.com"); + }); it("should set use ResultTotalsCalculator to calculate the totals", () => { const calculateTotals = (result) => { - expect(result.name).to.equal(report.name) - expect(result.data).to.be.an("array") - return { "visits": 1234 } - } - const GoogleAnalyticsDataProcessor = proxyquire("../../src/process-results/ga-data-processor", { - "./config": config, - "./result-totals-calculator": { calculateTotals }, - }) - - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) - expect(result.totals).to.deep.equal({ "visits": 1234 }) - }) - }) -}) + expect(result.name).to.equal(report.name); + expect(result.data).to.be.an("array"); + return { visits: 1234 }; + }; + const GoogleAnalyticsDataProcessor = proxyquire( + "../../src/process-results/ga-data-processor", + { + "./config": config, + "./result-totals-calculator": { calculateTotals }, + }, + ); + + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); + expect(result.totals).to.deep.equal({ visits: 1234 }); + }); + }); +}); diff --git a/ua/test/process-results/result-formatter.test.js b/ua/test/process-results/result-formatter.test.js index 5e370d4c..38669556 100644 --- a/ua/test/process-results/result-formatter.test.js +++ b/ua/test/process-results/result-formatter.test.js @@ -1,12 +1,15 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") -const reportFixture = require("../support/fixtures/report") -const dataFixture = require("../support/fixtures/data") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const reportFixture = require("../support/fixtures/report"); +const dataFixture = require("../support/fixtures/data"); -const GoogleAnalyticsDataProcessor = proxyquire("../../src/process-results/ga-data-processor", { - "../config": { account: { hostname: "" } }, -}) -const ResultFormatter = require("../../src/process-results/result-formatter") +const GoogleAnalyticsDataProcessor = proxyquire( + "../../src/process-results/ga-data-processor", + { + "../config": { account: { hostname: "" } }, + }, +); +const ResultFormatter = require("../../src/process-results/result-formatter"); describe("UA ResultFormatter", () => { describe("formatResult(result, options)", () => { @@ -15,43 +18,59 @@ describe("UA ResultFormatter", () => { let responseData; beforeEach(() => { - report = Object.assign({}, reportFixture) - data = Object.assign({}, dataFixture) - responseData = { data: data } - }) - - it("should format results into JSON if the format is 'json'", done => { - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) - - ResultFormatter.formatResult(result, { format: "json" }).then(formattedResult => { - const object = JSON.parse(formattedResult) - expect(object).to.deep.equal(object) - done() - }).catch(done) - }) - - it("should remove the data attribute for JSON if options.slim is true", done => { - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) - - ResultFormatter.formatResult(result, { format: "json", slim: true }).then(formattedResult => { - const object = JSON.parse(formattedResult) - expect(object.data).to.be.undefined - done() - }).catch(done) - }) + report = Object.assign({}, reportFixture); + data = Object.assign({}, dataFixture); + responseData = { data: data }; + }); + + it("should format results into JSON if the format is 'json'", (done) => { + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); + + ResultFormatter.formatResult(result, { format: "json" }) + .then((formattedResult) => { + const object = JSON.parse(formattedResult); + expect(object).to.deep.equal(object); + done(); + }) + .catch(done); + }); + + it("should remove the data attribute for JSON if options.slim is true", (done) => { + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); + + ResultFormatter.formatResult(result, { format: "json", slim: true }) + .then((formattedResult) => { + const object = JSON.parse(formattedResult); + expect(object.data).to.be.undefined; + done(); + }) + .catch(done); + }); it("should format results into CSV if the format is 'csv'", () => { - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); - return ResultFormatter.formatResult(result, { format: "csv", slim: true }).then(formattedResult => { + return ResultFormatter.formatResult(result, { + format: "csv", + slim: true, + }).then((formattedResult) => { const lines = formattedResult.split("\n"); - const [header, ...rows] = lines - expect(header).to.equal("date,hour,visits") - rows.forEach(row => { + const [header, ...rows] = lines; + expect(header).to.equal("date,hour,visits"); + rows.forEach((row) => { // Each CSV row should match 2017-01-30,00,100 - expect(row).to.match(/[0-9]{4}-[0-9]{2}-[0-9]{2},[0-9]{2},100/) + expect(row).to.match(/[0-9]{4}-[0-9]{2}-[0-9]{2},[0-9]{2},100/); }); - }) - }) - }) -}) + }); + }); + }); +}); diff --git a/ua/test/process-results/result-totals-calculator.test.js b/ua/test/process-results/result-totals-calculator.test.js index 4ab0735e..f9ccf49d 100644 --- a/ua/test/process-results/result-totals-calculator.test.js +++ b/ua/test/process-results/result-totals-calculator.test.js @@ -1,14 +1,17 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") -const reportFixture = require("../support/fixtures/report") -const dataFixture = require("../support/fixtures/data") -const ResultTotalsCalculator = require("../../src/process-results/result-totals-calculator") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const reportFixture = require("../support/fixtures/report"); +const dataFixture = require("../support/fixtures/data"); +const ResultTotalsCalculator = require("../../src/process-results/result-totals-calculator"); -proxyquire.noCallThru() +proxyquire.noCallThru(); -const GoogleAnalyticsDataProcessor = proxyquire("../../src/process-results/ga-data-processor", { - "../config": { account: { hostname: "" } }, -}) +const GoogleAnalyticsDataProcessor = proxyquire( + "../../src/process-results/ga-data-processor", + { + "../config": { account: { hostname: "" } }, + }, +); describe("UA ResultTotalsCalculator", () => { describe("calculateTotals(result)", () => { @@ -20,77 +23,89 @@ describe("UA ResultTotalsCalculator", () => { report = Object.assign({}, reportFixture); data = Object.assign({}, dataFixture); responseData = { data: data }; - }) + }); it("should compute totals for users", () => { - data.columnHeaders = [{ name: "ga:users" }] - data.rows = [["10"], ["15"], ["20"]] + data.columnHeaders = [{ name: "ga:users" }]; + data.rows = [["10"], ["15"], ["20"]]; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.users).to.equal(10 + 15 + 20) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.users).to.equal(10 + 15 + 20); + }); it("should compute totals for visits", () => { - data.columnHeaders = [{ name: "ga:sessions" }] - data.rows = [["10"], ["15"], ["20"]] + data.columnHeaders = [{ name: "ga:sessions" }]; + data.rows = [["10"], ["15"], ["20"]]; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.visits).to.equal(10 + 15 + 20) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.visits).to.equal(10 + 15 + 20); + }); it("should compute totals for device_models", () => { - report.name = "device_model" + report.name = "device_model"; data.columnHeaders = [ { name: "ga:date" }, { name: "ga:mobileDeviceModel" }, { name: "ga:sessions" }, - ] + ]; data.rows = [ ["20170130", "iPhone", "100"], ["20170130", "Android", "200"], ["20170131", "iPhone", "300"], ["20170131", "Android", "400"], - ] + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.device_models.iPhone).to.equal(100 + 300) - expect(totals.device_models.Android).to.equal(200 + 400) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.device_models.iPhone).to.equal(100 + 300); + expect(totals.device_models.Android).to.equal(200 + 400); + }); it("should compute totals for languages", () => { - report.name = "language" + report.name = "language"; data.columnHeaders = [ { name: "ga:date" }, { name: "ga:language" }, { name: "ga:sessions" }, - ] + ]; data.rows = [ ["20170130", "en", "100"], ["20170130", "es", "200"], ["20170131", "en", "300"], ["20170131", "es", "400"], - ] + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.languages.en).to.equal(100 + 300) - expect(totals.languages.es).to.equal(200 + 400) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.languages.en).to.equal(100 + 300); + expect(totals.languages.es).to.equal(200 + 400); + }); it("should compute totals for devices", () => { - report.name = "devices" + report.name = "devices"; data.columnHeaders = [ { name: "ga:date" }, { name: "ga:deviceCategory" }, { name: "ga:sessions" }, - ] + ]; data.rows = [ ["20170130", "mobile", "100"], ["20170130", "tablet", "200"], @@ -98,129 +113,147 @@ describe("UA ResultTotalsCalculator", () => { ["20170131", "mobile", "400"], ["20170131", "tablet", "500"], ["20170131", "desktop", "600"], - ] + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.devices.mobile).to.equal(100 + 400) - expect(totals.devices.tablet).to.equal(200 + 500) - expect(totals.devices.desktop).to.equal(300 + 600) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.devices.mobile).to.equal(100 + 400); + expect(totals.devices.tablet).to.equal(200 + 500); + expect(totals.devices.desktop).to.equal(300 + 600); + }); it("should compute totals for screen-sizes", () => { - report.name = "screen-size" + report.name = "screen-size"; data.columnHeaders = [ { name: "ga:date" }, { name: "ga:screenResolution" }, { name: "ga:sessions" }, - ] + ]; data.rows = [ ["20170130", "100x100", "100"], ["20170130", "200x200", "200"], ["20170131", "100x100", "300"], ["20170131", "200x200", "400"], - ] + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.screen_resolution["100x100"]).to.equal(100 + 300) - expect(totals.screen_resolution["200x200"]).to.equal(200 + 400) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.screen_resolution["100x100"]).to.equal(100 + 300); + expect(totals.screen_resolution["200x200"]).to.equal(200 + 400); + }); it("should compute totals for os", () => { - report.name = "os" + report.name = "os"; data.columnHeaders = [ { name: "ga:date" }, { name: "ga:operatingSystem" }, { name: "ga:sessions" }, - ] + ]; data.rows = [ ["20170130", "Nintendo Wii", "100"], ["20170130", "Xbox", "200"], ["20170131", "Nintendo Wii", "300"], ["20170131", "Xbox", "400"], - ] + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.os["Nintendo Wii"]).to.equal(100 + 300) - expect(totals.os["Xbox"]).to.equal(200 + 400) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.os["Nintendo Wii"]).to.equal(100 + 300); + expect(totals.os["Xbox"]).to.equal(200 + 400); + }); it("should compute totals for windows", () => { - report.name = "windows" + report.name = "windows"; data.columnHeaders = [ { name: "ga:date" }, { name: "ga:operatingSystemVersion" }, { name: "ga:sessions" }, - ] + ]; data.rows = [ ["20170130", "Server", "100"], ["20170130", "Vista", "200"], ["20170131", "Server", "300"], ["20170131", "Vista", "400"], - ] + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.os_version.Server).to.equal(100 + 300) - expect(totals.os_version.Vista).to.equal(200 + 400) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.os_version.Server).to.equal(100 + 300); + expect(totals.os_version.Vista).to.equal(200 + 400); + }); it("should compute totals for browsers", () => { - report.name = "browsers" + report.name = "browsers"; data.columnHeaders = [ { name: "ga:date" }, { name: "ga:browser" }, { name: "ga:sessions" }, - ] + ]; data.rows = [ ["20170130", "Chrome", "100"], ["20170130", "Safari", "200"], ["20170131", "Chrome", "300"], ["20170131", "Safari", "400"], - ] + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.browser.Chrome).to.equal(100 + 300) - expect(totals.browser.Safari).to.equal(200 + 400) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.browser.Chrome).to.equal(100 + 300); + expect(totals.browser.Safari).to.equal(200 + 400); + }); it("should compute totals for ie", () => { - report.name = "ie" + report.name = "ie"; data.columnHeaders = [ { name: "ga:date" }, { name: "ga:browserVersion" }, { name: "ga:sessions" }, - ] + ]; data.rows = [ ["20170130", "10.0", "100"], ["20170130", "11.0", "200"], ["20170131", "10.0", "300"], ["20170131", "11.0", "400"], - ] + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); - const totals = ResultTotalsCalculator.calculateTotals(result) - expect(totals.ie_version["10.0"]).to.equal(100 + 300) - expect(totals.ie_version["11.0"]).to.equal(200 + 400) - }) + const totals = ResultTotalsCalculator.calculateTotals(result); + expect(totals.ie_version["10.0"]).to.equal(100 + 300); + expect(totals.ie_version["11.0"]).to.equal(200 + 400); + }); it("should compute totals for os-browsers by operating system and browser", () => { - report.name = "os-browsers" + report.name = "os-browsers"; data.columnHeaders = [ { name: "ga:date" }, { name: "ga:operatingSystem" }, { name: "ga:browser" }, { name: "ga:sessions" }, - ] + ]; data.rows = [ ["20170130", "Windows", "Chrome", "100"], ["20170130", "Windows", "Firefox", "200"], @@ -230,27 +263,30 @@ describe("UA ResultTotalsCalculator", () => { ["20170130", "Windows", "Firefox", "600"], ["20170130", "Linux", "Chrome", "700"], ["20170130", "Linux", "Firefox", "800"], - ] + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); - const totals = ResultTotalsCalculator.calculateTotals(result) + const totals = ResultTotalsCalculator.calculateTotals(result); - expect(totals.by_os.Windows.Chrome).to.equal(100 + 500) - expect(totals.by_os.Windows.Firefox).to.equal(200 + 600) + expect(totals.by_os.Windows.Chrome).to.equal(100 + 500); + expect(totals.by_os.Windows.Firefox).to.equal(200 + 600); - expect(totals.by_browsers.Chrome.Windows).to.equal(100 + 500) - expect(totals.by_browsers.Chrome.Linux).to.equal(300 + 700) - }) + expect(totals.by_browsers.Chrome.Windows).to.equal(100 + 500); + expect(totals.by_browsers.Chrome.Linux).to.equal(300 + 700); + }); it("should compute totals for windows-ie by Windows version and IE version", () => { - report.name = "windows-ie" + report.name = "windows-ie"; data.columnHeaders = [ { name: "ga:date" }, { name: "ga:operatingSystemVersion" }, { name: "ga:browserVersion" }, { name: "ga:sessions" }, - ] + ]; data.rows = [ ["20170130", "XP", "10", "100"], ["20170130", "XP", "7", "200"], @@ -260,27 +296,30 @@ describe("UA ResultTotalsCalculator", () => { ["20170130", "XP", "7", "600"], ["20170130", "Vista", "10", "700"], ["20170130", "Vista", "7", "800"], - ] + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); - const totals = ResultTotalsCalculator.calculateTotals(result) + const totals = ResultTotalsCalculator.calculateTotals(result); - expect(totals.by_windows.XP["10"]).to.equal(100 + 500) - expect(totals.by_windows.XP["7"]).to.equal(200 + 600) + expect(totals.by_windows.XP["10"]).to.equal(100 + 500); + expect(totals.by_windows.XP["7"]).to.equal(200 + 600); - expect(totals.by_ie["10"].XP).to.equal(100 + 500) - expect(totals.by_ie["10"].Vista).to.equal(300 + 700) - }) + expect(totals.by_ie["10"].XP).to.equal(100 + 500); + expect(totals.by_ie["10"].Vista).to.equal(300 + 700); + }); it("should compute totals for windows-browsers by windows version and browser version", () => { - report.name = "windows-browsers" + report.name = "windows-browsers"; data.columnHeaders = [ { name: "ga:date" }, { name: "ga:operatingSystemVersion" }, { name: "ga:browser" }, { name: "ga:sessions" }, - ] + ]; data.rows = [ ["20170130", "XP", "Chrome", "100"], ["20170130", "XP", "Firefox", "200"], @@ -290,17 +329,20 @@ describe("UA ResultTotalsCalculator", () => { ["20170130", "XP", "Firefox", "600"], ["20170130", "Vista", "Chrome", "700"], ["20170130", "Vista", "Firefox", "800"], - ] + ]; - const result = GoogleAnalyticsDataProcessor.processData(report, responseData) + const result = GoogleAnalyticsDataProcessor.processData( + report, + responseData, + ); - const totals = ResultTotalsCalculator.calculateTotals(result) + const totals = ResultTotalsCalculator.calculateTotals(result); - expect(totals.by_windows.XP.Chrome).to.equal(100 + 500) - expect(totals.by_windows.XP.Firefox).to.equal(200 + 600) + expect(totals.by_windows.XP.Chrome).to.equal(100 + 500); + expect(totals.by_windows.XP.Firefox).to.equal(200 + 600); - expect(totals.by_browsers.Chrome.XP).to.equal(100 + 500) - expect(totals.by_browsers.Chrome.Vista).to.equal(300 + 700) - }) - }) -}) + expect(totals.by_browsers.Chrome.XP).to.equal(100 + 500); + expect(totals.by_browsers.Chrome.Vista).to.equal(300 + 700); + }); + }); +}); diff --git a/ua/test/publish/disk.test.js b/ua/test/publish/disk.test.js index d0756282..86944e51 100644 --- a/ua/test/publish/disk.test.js +++ b/ua/test/publish/disk.test.js @@ -1,58 +1,62 @@ -const expect = require("chai").expect -const proxyquire = require("proxyquire") +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); describe("UA DiskPublisher", () => { - let DiskPublisher - let fs = {} + let DiskPublisher; + let fs = {}; beforeEach(() => { - fs = { writeFile: (path, contents, cb) => cb() } + fs = { writeFile: (path, contents, cb) => cb() }; DiskPublisher = proxyquire("../../src/publish/disk", { fs: fs, - }) - }) + }); + }); describe(".publish(report, results, options)", () => { context("when the format is json", () => { - it("should write the results to /.json", done => { - const options = { output: "path/to/output", format: "json" } - const report = { name: "report-name" } - const results = "I'm the results" + it("should write the results to /.json", (done) => { + const options = { output: "path/to/output", format: "json" }; + const report = { name: "report-name" }; + const results = "I'm the results"; - let fileWritten = false + let fileWritten = false; fs.writeFile = (path, contents, cb) => { - expect(path).to.equal("path/to/output/report-name.json") - expect(contents).to.equal("I'm the results") - fileWritten = true - cb(null) - } - - DiskPublisher.publish(report, results, options).then(() => { - expect(fileWritten).to.be.true - done() - }).catch(done) - }) - }) + expect(path).to.equal("path/to/output/report-name.json"); + expect(contents).to.equal("I'm the results"); + fileWritten = true; + cb(null); + }; + + DiskPublisher.publish(report, results, options) + .then(() => { + expect(fileWritten).to.be.true; + done(); + }) + .catch(done); + }); + }); context("when the format is csv", () => { - it("should write the results to /.csv", done => { - const options = { output: "path/to/output", format: "csv" } - const report = { name: "report-name" } - const results = "I'm the results" + it("should write the results to /.csv", (done) => { + const options = { output: "path/to/output", format: "csv" }; + const report = { name: "report-name" }; + const results = "I'm the results"; - let fileWritten = false + let fileWritten = false; fs.writeFile = (path, contents, cb) => { - expect(path).to.equal("path/to/output/report-name.csv") - expect(contents).to.equal("I'm the results") - fileWritten = true - cb(null) - } - - DiskPublisher.publish(report, results, options).then(() => { - expect(fileWritten).to.be.true - done() - }).catch(done) - }) - }) - }) -}) + expect(path).to.equal("path/to/output/report-name.csv"); + expect(contents).to.equal("I'm the results"); + fileWritten = true; + cb(null); + }; + + DiskPublisher.publish(report, results, options) + .then(() => { + expect(fileWritten).to.be.true; + done(); + }) + .catch(done); + }); + }); + }); +}); diff --git a/ua/test/publish/postgres.test.js b/ua/test/publish/postgres.test.js index 57ecd524..5bb5c3be 100644 --- a/ua/test/publish/postgres.test.js +++ b/ua/test/publish/postgres.test.js @@ -1,40 +1,39 @@ -const { ANALYTICS_DATA_TABLE_NAME } = require("../../src/publish/postgres") +const { ANALYTICS_DATA_TABLE_NAME } = require("../../src/publish/postgres"); -const expect = require("chai").expect -const knex = require("knex") -const proxyquire = require("proxyquire") -const database = require("../support/database") -const resultsFixture = require("../support/fixtures/results") +const expect = require("chai").expect; +const knex = require("knex"); +const proxyquire = require("proxyquire"); +const database = require("../support/database"); +const resultsFixture = require("../support/fixtures/results"); -proxyquire.noCallThru() +proxyquire.noCallThru(); const PostgresPublisher = proxyquire("../../src/publish/postgres", { - "../config": require('../../src/config'), -}) + "../config": require("../../src/config"), +}); describe("UA PostgresPublisher", () => { - let databaseClient, results + let databaseClient, results; before((done) => { // Setup the database client - databaseClient = knex({ client: "pg", connection: database.connection }) - done() + databaseClient = knex({ client: "pg", connection: database.connection }); + done(); }); - after((done) => { // Clean up the database client databaseClient.destroy().then(() => done()); }); beforeEach((done) => { - results = Object.assign({}, resultsFixture) - database.resetSchema(databaseClient).then(() => done()) - }) + results = Object.assign({}, resultsFixture); + database.resetSchema(databaseClient).then(() => done()); + }); describe(".publish(results)", () => { - it("should insert a record for each results.data element", done => { - results.name = "report-name" + it("should insert a record for each results.data element", (done) => { + results.name = "report-name"; results.data = [ { date: "2017-02-11", @@ -44,57 +43,70 @@ describe("UA PostgresPublisher", () => { date: "2017-02-12", name: "def", }, - ] - - PostgresPublisher.publish(results).then(() => { - return databaseClient(ANALYTICS_DATA_TABLE_NAME).orderBy("date", "asc").select() - }).then(rows => { - expect(rows).to.have.length(2) - rows.forEach((row, index) => { - const data = results.data[index] - expect(row.report_name).to.equal("report-name") - expect(row.data.name).to.equal(data.name) - expect(row.date.toISOString()).to.match(RegExp(`^${data.date}`)) + ]; + + PostgresPublisher.publish(results) + .then(() => { + return databaseClient(ANALYTICS_DATA_TABLE_NAME) + .orderBy("date", "asc") + .select(); + }) + .then((rows) => { + expect(rows).to.have.length(2); + rows.forEach((row, index) => { + const data = results.data[index]; + expect(row.report_name).to.equal("report-name"); + expect(row.data.name).to.equal(data.name); + expect(row.date.toISOString()).to.match(RegExp(`^${data.date}`)); + }); + done(); + }) + .catch(done); + }); + + it("should coerce certain values into numbers", (done) => { + results.name = "report-name"; + results.data = [ + { + date: "2017-05-15", + name: "abc", + visits: "123", + total_events: "456", + }, + ]; + + PostgresPublisher.publish(results) + .then(() => { + return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME); }) - done() - }).catch(done) - }) - - it("should coerce certain values into numbers", done => { - results.name = "report-name" - results.data = [{ - date: "2017-05-15", - name: "abc", - visits: "123", - total_events: "456", - }] - - PostgresPublisher.publish(results).then(() => { - return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME) - }).then(rows => { - const row = rows[0] - expect(row.data.visits).to.be.a("number") - expect(row.data.visits).to.equal(123) - expect(row.data.total_events).to.be.a("number") - expect(row.data.total_events).to.equal(456) - done() - }).catch(done) - }) - - it("should ignore reports that don't have a ga:date dimension", done => { - results.query = { dimensions: "ga:something,ga:somethingElse" } - - PostgresPublisher.publish(results).then(() => { - return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME) - }).then(rows => { - expect(rows).to.have.length(0) - done() - }).catch(done) - }) - - it("should ignore data points that have already been inserted", done => { - firstResults = Object.assign({}, results) - secondResults = Object.assign({}, results) + .then((rows) => { + const row = rows[0]; + expect(row.data.visits).to.be.a("number"); + expect(row.data.visits).to.equal(123); + expect(row.data.total_events).to.be.a("number"); + expect(row.data.total_events).to.equal(456); + done(); + }) + .catch(done); + }); + + it("should ignore reports that don't have a ga:date dimension", (done) => { + results.query = { dimensions: "ga:something,ga:somethingElse" }; + + PostgresPublisher.publish(results) + .then(() => { + return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME); + }) + .then((rows) => { + expect(rows).to.have.length(0); + done(); + }) + .catch(done); + }); + + it("should ignore data points that have already been inserted", (done) => { + firstResults = Object.assign({}, results); + secondResults = Object.assign({}, results); firstResults.data = [ { @@ -105,9 +117,9 @@ describe("UA PostgresPublisher", () => { { date: "2017-02-11", visits: "456", - browser: "Safari" + browser: "Safari", }, - ] + ]; secondResults.data = [ { date: "2017-02-11", @@ -117,23 +129,27 @@ describe("UA PostgresPublisher", () => { { date: "2017-02-11", visits: "789", - browser: "Internet Explorer" + browser: "Internet Explorer", }, - ] - - PostgresPublisher.publish(firstResults).then(() => { - return PostgresPublisher.publish(secondResults) - }).then(() => { - return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME) - }).then(rows => { - expect(rows).to.have.length(3) - done() - }).catch(done) - }) - - it("should overwrite existing data points if the number of visits or users has changed", done => { - firstResults = Object.assign({}, results) - secondResults = Object.assign({}, results) + ]; + + PostgresPublisher.publish(firstResults) + .then(() => { + return PostgresPublisher.publish(secondResults); + }) + .then(() => { + return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME); + }) + .then((rows) => { + expect(rows).to.have.length(3); + done(); + }) + .catch(done); + }); + + it("should overwrite existing data points if the number of visits or users has changed", (done) => { + firstResults = Object.assign({}, results); + secondResults = Object.assign({}, results); firstResults.data = [ { @@ -146,7 +162,7 @@ describe("UA PostgresPublisher", () => { total_events: "300", title: "IRS Form 123", }, - ] + ]; secondResults.data = [ { date: "2017-02-11", @@ -158,26 +174,30 @@ describe("UA PostgresPublisher", () => { total_events: "400", title: "IRS Form 123", }, - ] - - PostgresPublisher.publish(firstResults).then(() => { - return PostgresPublisher.publish(secondResults) - }).then(() => { - return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME) - }).then(rows => { - expect(rows).to.have.length(2) - rows.forEach(row => { - if (row.data.visits) { - expect(row.data.visits).to.equal(200) - } else { - expect(row.data.total_events).to.equal(400) - } + ]; + + PostgresPublisher.publish(firstResults) + .then(() => { + return PostgresPublisher.publish(secondResults); + }) + .then(() => { + return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME); }) - done() - }).catch(done) - }) + .then((rows) => { + expect(rows).to.have.length(2); + rows.forEach((row) => { + if (row.data.visits) { + expect(row.data.visits).to.equal(200); + } else { + expect(row.data.total_events).to.equal(400); + } + }); + done(); + }) + .catch(done); + }); - it("should not not insert a record if the date is invalid", done => { + it("should not not insert a record if the date is invalid", (done) => { results.data = [ { date: "(other)", @@ -187,15 +207,18 @@ describe("UA PostgresPublisher", () => { date: "2017-02-16", visits: "456", }, - ] - - PostgresPublisher.publish(results).then(() => { - return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME) - }).then(rows => { - expect(rows).to.have.length(1) - expect(rows[0].data.visits).to.equal(456) - done() - }).catch(done) - }) - }) -}) + ]; + + PostgresPublisher.publish(results) + .then(() => { + return databaseClient.select().table(ANALYTICS_DATA_TABLE_NAME); + }) + .then((rows) => { + expect(rows).to.have.length(1); + expect(rows[0].data.visits).to.equal(456); + done(); + }) + .catch(done); + }); + }); +}); diff --git a/ua/test/support/database.js b/ua/test/support/database.js index 0f32b2e1..7a16f26c 100644 --- a/ua/test/support/database.js +++ b/ua/test/support/database.js @@ -1,10 +1,9 @@ -const { ANALYTICS_DATA_TABLE_NAME } = require("../../src/publish/postgres") +const { ANALYTICS_DATA_TABLE_NAME } = require("../../src/publish/postgres"); -const config = require('../../src/config') +const config = require("../../src/config"); const resetSchema = (db) => { return db(ANALYTICS_DATA_TABLE_NAME).delete(); -} - -module.exports = { connection: config.postgres, resetSchema } +}; +module.exports = { connection: config.postgres, resetSchema }; diff --git a/ua/test/support/fixtures/data.js b/ua/test/support/fixtures/data.js index 0ad15a4a..18fd3b53 100644 --- a/ua/test/support/fixtures/data.js +++ b/ua/test/support/fixtures/data.js @@ -1,30 +1,42 @@ module.exports = { - kind: 'analytics#gaData', - id: 'https://www.googleapis.com/analytics/v3/data/ga?ids=ga:96302018&dimensions=ga:date,ga:hour&metrics=ga:sessions&start-date=today&end-date=today&max-results=10000', + kind: "analytics#gaData", + id: "https://www.googleapis.com/analytics/v3/data/ga?ids=ga:96302018&dimensions=ga:date,ga:hour&metrics=ga:sessions&start-date=today&end-date=today&max-results=10000", query: { - 'start-date': 'today', 'end-date': 'today', ids: 'ga:96302018', - dimensions: 'ga:date,ga:hour', metrics: [ 'ga:sessions' ], - 'start-index': 1, 'max-results': 10000, samplingLevel: 'HIGHER_PRECISION', + "start-date": "today", + "end-date": "today", + ids: "ga:96302018", + dimensions: "ga:date,ga:hour", + metrics: ["ga:sessions"], + "start-index": 1, + "max-results": 10000, + samplingLevel: "HIGHER_PRECISION", }, itemsPerPage: 10000, totalResults: 24, - selfLink: 'https://www.googleapis.com/analytics/v3/data/ga?ids=ga:96302018&dimensions=ga:date,ga:hour&metrics=ga:sessions&start-date=today&end-date=today&max-results=10000', + selfLink: + "https://www.googleapis.com/analytics/v3/data/ga?ids=ga:96302018&dimensions=ga:date,ga:hour&metrics=ga:sessions&start-date=today&end-date=today&max-results=10000", profileInfo: { - profileId: '96302018', - accountId: '33523145', - webPropertyId: 'UA-33523145-1', - internalWebPropertyId: '60822123', - profileName: 'Z3. Adjusted Gov-Wide Reporting Profile (.gov & .mil only)', - tableId: 'ga:96302018' + profileId: "96302018", + accountId: "33523145", + webPropertyId: "UA-33523145-1", + internalWebPropertyId: "60822123", + profileName: "Z3. Adjusted Gov-Wide Reporting Profile (.gov & .mil only)", + tableId: "ga:96302018", }, containsSampledData: false, columnHeaders: [ - { name: 'ga:date', columnType: 'DIMENSION', dataType: 'STRING' }, - { name: 'ga:hour', columnType: 'DIMENSION', dataType: 'STRING' }, - { name: 'ga:sessions', columnType: 'METRIC', dataType: 'INTEGER' } + { name: "ga:date", columnType: "DIMENSION", dataType: "STRING" }, + { name: "ga:hour", columnType: "DIMENSION", dataType: "STRING" }, + { name: "ga:sessions", columnType: "METRIC", dataType: "INTEGER" }, ], - totalsForAllResults: { 'ga:sessions': '6782212' }, - rows: Array(24).fill(100).map((val, index) => { - return ["20170130", `${index}`.length < 2 ? `0${index}` : `${index}`, `${val}`] - }), -} + totalsForAllResults: { "ga:sessions": "6782212" }, + rows: Array(24) + .fill(100) + .map((val, index) => { + return [ + "20170130", + `${index}`.length < 2 ? `0${index}` : `${index}`, + `${val}`, + ]; + }), +}; diff --git a/ua/test/support/fixtures/data_with_hostname.js b/ua/test/support/fixtures/data_with_hostname.js index 7ecc1755..bdf08ddc 100644 --- a/ua/test/support/fixtures/data_with_hostname.js +++ b/ua/test/support/fixtures/data_with_hostname.js @@ -1,31 +1,44 @@ module.exports = { - kind: 'analytics#gaData', - id: 'https://www.googleapis.com/analytics/v3/data/ga?ids=ga:96302018&dimensions=ga:date,ga:hour&metrics=ga:sessions&start-date=today&end-date=today&max-results=10000', + kind: "analytics#gaData", + id: "https://www.googleapis.com/analytics/v3/data/ga?ids=ga:96302018&dimensions=ga:date,ga:hour&metrics=ga:sessions&start-date=today&end-date=today&max-results=10000", query: { - 'start-date': 'today', 'end-date': 'today', ids: 'ga:96302018', - dimensions: 'ga:date,ga:hour', metrics: [ 'ga:sessions' ], - 'start-index': 1, 'max-results': 10000, samplingLevel: 'HIGHER_PRECISION', + "start-date": "today", + "end-date": "today", + ids: "ga:96302018", + dimensions: "ga:date,ga:hour", + metrics: ["ga:sessions"], + "start-index": 1, + "max-results": 10000, + samplingLevel: "HIGHER_PRECISION", }, itemsPerPage: 10000, totalResults: 24, - selfLink: 'https://www.googleapis.com/analytics/v3/data/ga?ids=ga:96302018&dimensions=ga:date,ga:hour&metrics=ga:sessions&start-date=today&end-date=today&max-results=10000', + selfLink: + "https://www.googleapis.com/analytics/v3/data/ga?ids=ga:96302018&dimensions=ga:date,ga:hour&metrics=ga:sessions&start-date=today&end-date=today&max-results=10000", profileInfo: { - profileId: '96302018', - accountId: '33523145', - webPropertyId: 'UA-33523145-1', - internalWebPropertyId: '60822123', - profileName: 'Z3. Adjusted Gov-Wide Reporting Profile (.gov & .mil only)', - tableId: 'ga:96302018' + profileId: "96302018", + accountId: "33523145", + webPropertyId: "UA-33523145-1", + internalWebPropertyId: "60822123", + profileName: "Z3. Adjusted Gov-Wide Reporting Profile (.gov & .mil only)", + tableId: "ga:96302018", }, containsSampledData: false, columnHeaders: [ - { name: 'ga:date', columnType: 'DIMENSION', dataType: 'STRING' }, - { name: 'ga:hour', columnType: 'DIMENSION', dataType: 'STRING' }, - { name: 'ga:hostname', columnType: 'DIMENSION', dataType: 'STRING' }, - { name: 'ga:sessions', columnType: 'METRIC', dataType: 'INTEGER' } + { name: "ga:date", columnType: "DIMENSION", dataType: "STRING" }, + { name: "ga:hour", columnType: "DIMENSION", dataType: "STRING" }, + { name: "ga:hostname", columnType: "DIMENSION", dataType: "STRING" }, + { name: "ga:sessions", columnType: "METRIC", dataType: "INTEGER" }, ], - totalsForAllResults: { 'ga:sessions': '6782212' }, - rows: Array(24).fill(100).map((val, index) => { - return ["20170130", `${index}`.length < 2 ? `0${index}` : `${index}`, `www.example${index}.com`,`${val}`] - }), -} + totalsForAllResults: { "ga:sessions": "6782212" }, + rows: Array(24) + .fill(100) + .map((val, index) => { + return [ + "20170130", + `${index}`.length < 2 ? `0${index}` : `${index}`, + `www.example${index}.com`, + `${val}`, + ]; + }), +}; diff --git a/ua/test/support/fixtures/report.js b/ua/test/support/fixtures/report.js index e6012a1e..218b6e8a 100644 --- a/ua/test/support/fixtures/report.js +++ b/ua/test/support/fixtures/report.js @@ -1,14 +1,14 @@ module.exports = { - "name": "today", - "frequency": "hourly", - "query": { - "dimensions": ["ga:date", "ga:hour"], - "metrics": ["ga:sessions"], + name: "today", + frequency: "hourly", + query: { + dimensions: ["ga:date", "ga:hour"], + metrics: ["ga:sessions"], "start-date": "today", "end-date": "today", }, - "meta": { - "name": "Today", - "description": "Today's visits for all sites." + meta: { + name: "Today", + description: "Today's visits for all sites.", }, -} +}; diff --git a/ua/test/support/fixtures/results.js b/ua/test/support/fixtures/results.js index fb6fbb3a..49e25c72 100644 --- a/ua/test/support/fixtures/results.js +++ b/ua/test/support/fixtures/results.js @@ -1,1383 +1,1378 @@ module.exports = { - "name": "devices", - "query": { + name: "devices", + query: { "start-date": "90daysAgo", "end-date": "yesterday", - "dimensions": "ga:date,ga:deviceCategory", - "metrics": [ - "ga:sessions" - ], - "sort": [ - "ga:date" - ], + dimensions: "ga:date,ga:deviceCategory", + metrics: ["ga:sessions"], + sort: ["ga:date"], "start-index": 1, "max-results": 10000, - "samplingLevel": "HIGHER_PRECISION" + samplingLevel: "HIGHER_PRECISION", }, - "meta": { - "name": "Devices", - "description": "90 days of desktop/mobile/tablet visits for all sites." + meta: { + name: "Devices", + description: "90 days of desktop/mobile/tablet visits for all sites.", }, - "data": [ + data: [ { - "date": "2016-11-17", - "device": "desktop", - "visits": "17944716" + date: "2016-11-17", + device: "desktop", + visits: "17944716", }, { - "date": "2016-11-17", - "device": "mobile", - "visits": "8927140" + date: "2016-11-17", + device: "mobile", + visits: "8927140", }, { - "date": "2016-11-17", - "device": "tablet", - "visits": "1493615" + date: "2016-11-17", + device: "tablet", + visits: "1493615", }, { - "date": "2016-11-18", - "device": "desktop", - "visits": "15266887" + date: "2016-11-18", + device: "desktop", + visits: "15266887", }, { - "date": "2016-11-18", - "device": "mobile", - "visits": "8188620" + date: "2016-11-18", + device: "mobile", + visits: "8188620", }, { - "date": "2016-11-18", - "device": "tablet", - "visits": "1359252" + date: "2016-11-18", + device: "tablet", + visits: "1359252", }, { - "date": "2016-11-19", - "device": "desktop", - "visits": "7486523" + date: "2016-11-19", + device: "desktop", + visits: "7486523", }, { - "date": "2016-11-19", - "device": "mobile", - "visits": "6802302" + date: "2016-11-19", + device: "mobile", + visits: "6802302", }, { - "date": "2016-11-19", - "device": "tablet", - "visits": "1244910" + date: "2016-11-19", + device: "tablet", + visits: "1244910", }, { - "date": "2016-11-20", - "device": "desktop", - "visits": "8095419" + date: "2016-11-20", + device: "desktop", + visits: "8095419", }, { - "date": "2016-11-20", - "device": "mobile", - "visits": "6355972" + date: "2016-11-20", + device: "mobile", + visits: "6355972", }, { - "date": "2016-11-20", - "device": "tablet", - "visits": "1301498" + date: "2016-11-20", + device: "tablet", + visits: "1301498", }, { - "date": "2016-11-21", - "device": "desktop", - "visits": "18290260" + date: "2016-11-21", + device: "desktop", + visits: "18290260", }, { - "date": "2016-11-21", - "device": "mobile", - "visits": "8660823" + date: "2016-11-21", + device: "mobile", + visits: "8660823", }, { - "date": "2016-11-21", - "device": "tablet", - "visits": "1478005" + date: "2016-11-21", + device: "tablet", + visits: "1478005", }, { - "date": "2016-11-22", - "device": "desktop", - "visits": "16994015" + date: "2016-11-22", + device: "desktop", + visits: "16994015", }, { - "date": "2016-11-22", - "device": "mobile", - "visits": "8599485" + date: "2016-11-22", + device: "mobile", + visits: "8599485", }, { - "date": "2016-11-22", - "device": "tablet", - "visits": "1413091" + date: "2016-11-22", + device: "tablet", + visits: "1413091", }, { - "date": "2016-11-23", - "device": "desktop", - "visits": "13510470" + date: "2016-11-23", + device: "desktop", + visits: "13510470", }, { - "date": "2016-11-23", - "device": "mobile", - "visits": "8133319" + date: "2016-11-23", + device: "mobile", + visits: "8133319", }, { - "date": "2016-11-23", - "device": "tablet", - "visits": "1279496" + date: "2016-11-23", + device: "tablet", + visits: "1279496", }, { - "date": "2016-11-24", - "device": "desktop", - "visits": "6234988" + date: "2016-11-24", + device: "desktop", + visits: "6234988", }, { - "date": "2016-11-24", - "device": "mobile", - "visits": "5953655" + date: "2016-11-24", + device: "mobile", + visits: "5953655", }, { - "date": "2016-11-24", - "device": "tablet", - "visits": "1022314" + date: "2016-11-24", + device: "tablet", + visits: "1022314", }, { - "date": "2016-11-25", - "device": "desktop", - "visits": "8768054" + date: "2016-11-25", + device: "desktop", + visits: "8768054", }, { - "date": "2016-11-25", - "device": "mobile", - "visits": "7241617" + date: "2016-11-25", + device: "mobile", + visits: "7241617", }, { - "date": "2016-11-25", - "device": "tablet", - "visits": "1212316" + date: "2016-11-25", + device: "tablet", + visits: "1212316", }, { - "date": "2016-11-26", - "device": "desktop", - "visits": "6981808" + date: "2016-11-26", + device: "desktop", + visits: "6981808", }, { - "date": "2016-11-26", - "device": "mobile", - "visits": "6722048" + date: "2016-11-26", + device: "mobile", + visits: "6722048", }, { - "date": "2016-11-26", - "device": "tablet", - "visits": "1190519" + date: "2016-11-26", + device: "tablet", + visits: "1190519", }, { - "date": "2016-11-27", - "device": "desktop", - "visits": "8225314" + date: "2016-11-27", + device: "desktop", + visits: "8225314", }, { - "date": "2016-11-27", - "device": "mobile", - "visits": "6672403" + date: "2016-11-27", + device: "mobile", + visits: "6672403", }, { - "date": "2016-11-27", - "device": "tablet", - "visits": "1302649" + date: "2016-11-27", + device: "tablet", + visits: "1302649", }, { - "date": "2016-11-28", - "device": "desktop", - "visits": "19526901" + date: "2016-11-28", + device: "desktop", + visits: "19526901", }, { - "date": "2016-11-28", - "device": "mobile", - "visits": "9300099" + date: "2016-11-28", + device: "mobile", + visits: "9300099", }, { - "date": "2016-11-28", - "device": "tablet", - "visits": "1547016" + date: "2016-11-28", + device: "tablet", + visits: "1547016", }, { - "date": "2016-11-29", - "device": "desktop", - "visits": "19881628" + date: "2016-11-29", + device: "desktop", + visits: "19881628", }, { - "date": "2016-11-29", - "device": "mobile", - "visits": "9665025" + date: "2016-11-29", + device: "mobile", + visits: "9665025", }, { - "date": "2016-11-29", - "device": "tablet", - "visits": "1579273" + date: "2016-11-29", + device: "tablet", + visits: "1579273", }, { - "date": "2016-11-30", - "device": "desktop", - "visits": "19573065" + date: "2016-11-30", + device: "desktop", + visits: "19573065", }, { - "date": "2016-11-30", - "device": "mobile", - "visits": "10083858" + date: "2016-11-30", + device: "mobile", + visits: "10083858", }, { - "date": "2016-11-30", - "device": "tablet", - "visits": "1601741" + date: "2016-11-30", + device: "tablet", + visits: "1601741", }, { - "date": "2016-12-01", - "device": "desktop", - "visits": "18611610" + date: "2016-12-01", + device: "desktop", + visits: "18611610", }, { - "date": "2016-12-01", - "device": "mobile", - "visits": "10212056" + date: "2016-12-01", + device: "mobile", + visits: "10212056", }, { - "date": "2016-12-01", - "device": "tablet", - "visits": "1564647" + date: "2016-12-01", + device: "tablet", + visits: "1564647", }, { - "date": "2016-12-02", - "device": "desktop", - "visits": "16303740" + date: "2016-12-02", + device: "desktop", + visits: "16303740", }, { - "date": "2016-12-02", - "device": "mobile", - "visits": "9595214" + date: "2016-12-02", + device: "mobile", + visits: "9595214", }, { - "date": "2016-12-02", - "device": "tablet", - "visits": "1452885" + date: "2016-12-02", + device: "tablet", + visits: "1452885", }, { - "date": "2016-12-03", - "device": "desktop", - "visits": "8145522" + date: "2016-12-03", + device: "desktop", + visits: "8145522", }, { - "date": "2016-12-03", - "device": "mobile", - "visits": "8038915" + date: "2016-12-03", + device: "mobile", + visits: "8038915", }, { - "date": "2016-12-03", - "device": "tablet", - "visits": "1328963" + date: "2016-12-03", + device: "tablet", + visits: "1328963", }, { - "date": "2016-12-04", - "device": "desktop", - "visits": "8753097" + date: "2016-12-04", + device: "desktop", + visits: "8753097", }, { - "date": "2016-12-04", - "device": "mobile", - "visits": "7206951" + date: "2016-12-04", + device: "mobile", + visits: "7206951", }, { - "date": "2016-12-04", - "device": "tablet", - "visits": "1365981" + date: "2016-12-04", + device: "tablet", + visits: "1365981", }, { - "date": "2016-12-05", - "device": "desktop", - "visits": "20527426" + date: "2016-12-05", + device: "desktop", + visits: "20527426", }, { - "date": "2016-12-05", - "device": "mobile", - "visits": "10433381" + date: "2016-12-05", + device: "mobile", + visits: "10433381", }, { - "date": "2016-12-05", - "device": "tablet", - "visits": "1670167" + date: "2016-12-05", + device: "tablet", + visits: "1670167", }, { - "date": "2016-12-06", - "device": "desktop", - "visits": "19967407" + date: "2016-12-06", + device: "desktop", + visits: "19967407", }, { - "date": "2016-12-06", - "device": "mobile", - "visits": "10023434" + date: "2016-12-06", + device: "mobile", + visits: "10023434", }, { - "date": "2016-12-06", - "device": "tablet", - "visits": "1657519" + date: "2016-12-06", + device: "tablet", + visits: "1657519", }, { - "date": "2016-12-07", - "device": "desktop", - "visits": "19532055" + date: "2016-12-07", + device: "desktop", + visits: "19532055", }, { - "date": "2016-12-07", - "device": "mobile", - "visits": "10063789" + date: "2016-12-07", + device: "mobile", + visits: "10063789", }, { - "date": "2016-12-07", - "device": "tablet", - "visits": "1646568" + date: "2016-12-07", + device: "tablet", + visits: "1646568", }, { - "date": "2016-12-08", - "device": "desktop", - "visits": "19218012" + date: "2016-12-08", + device: "desktop", + visits: "19218012", }, { - "date": "2016-12-08", - "device": "mobile", - "visits": "10323528" + date: "2016-12-08", + device: "mobile", + visits: "10323528", }, { - "date": "2016-12-08", - "device": "tablet", - "visits": "1714556" + date: "2016-12-08", + device: "tablet", + visits: "1714556", }, { - "date": "2016-12-09", - "device": "desktop", - "visits": "16651672" + date: "2016-12-09", + device: "desktop", + visits: "16651672", }, { - "date": "2016-12-09", - "device": "mobile", - "visits": "9478158" + date: "2016-12-09", + device: "mobile", + visits: "9478158", }, { - "date": "2016-12-09", - "device": "tablet", - "visits": "1564344" + date: "2016-12-09", + device: "tablet", + visits: "1564344", }, { - "date": "2016-12-10", - "device": "desktop", - "visits": "8394504" + date: "2016-12-10", + device: "desktop", + visits: "8394504", }, { - "date": "2016-12-10", - "device": "mobile", - "visits": "8008296" + date: "2016-12-10", + device: "mobile", + visits: "8008296", }, { - "date": "2016-12-10", - "device": "tablet", - "visits": "1438817" + date: "2016-12-10", + device: "tablet", + visits: "1438817", }, { - "date": "2016-12-11", - "device": "desktop", - "visits": "8769674" + date: "2016-12-11", + device: "desktop", + visits: "8769674", }, { - "date": "2016-12-11", - "device": "mobile", - "visits": "7318707" + date: "2016-12-11", + device: "mobile", + visits: "7318707", }, { - "date": "2016-12-11", - "device": "tablet", - "visits": "1471781" + date: "2016-12-11", + device: "tablet", + visits: "1471781", }, { - "date": "2016-12-12", - "device": "desktop", - "visits": "20124799" + date: "2016-12-12", + device: "desktop", + visits: "20124799", }, { - "date": "2016-12-12", - "device": "mobile", - "visits": "10002557" + date: "2016-12-12", + device: "mobile", + visits: "10002557", }, { - "date": "2016-12-12", - "device": "tablet", - "visits": "1677637" + date: "2016-12-12", + device: "tablet", + visits: "1677637", }, { - "date": "2016-12-13", - "device": "desktop", - "visits": "19692582" + date: "2016-12-13", + device: "desktop", + visits: "19692582", }, { - "date": "2016-12-13", - "device": "mobile", - "visits": "9946246" + date: "2016-12-13", + device: "mobile", + visits: "9946246", }, { - "date": "2016-12-13", - "device": "tablet", - "visits": "1664839" + date: "2016-12-13", + device: "tablet", + visits: "1664839", }, { - "date": "2016-12-14", - "device": "desktop", - "visits": "19450673" + date: "2016-12-14", + device: "desktop", + visits: "19450673", }, { - "date": "2016-12-14", - "device": "mobile", - "visits": "10324397" + date: "2016-12-14", + device: "mobile", + visits: "10324397", }, { - "date": "2016-12-14", - "device": "tablet", - "visits": "1713116" + date: "2016-12-14", + device: "tablet", + visits: "1713116", }, { - "date": "2016-12-15", - "device": "desktop", - "visits": "19047361" + date: "2016-12-15", + device: "desktop", + visits: "19047361", }, { - "date": "2016-12-15", - "device": "mobile", - "visits": "10346150" + date: "2016-12-15", + device: "mobile", + visits: "10346150", }, { - "date": "2016-12-15", - "device": "tablet", - "visits": "1728800" + date: "2016-12-15", + device: "tablet", + visits: "1728800", }, { - "date": "2016-12-16", - "device": "desktop", - "visits": "16873358" + date: "2016-12-16", + device: "desktop", + visits: "16873358", }, { - "date": "2016-12-16", - "device": "mobile", - "visits": "9932215" + date: "2016-12-16", + device: "mobile", + visits: "9932215", }, { - "date": "2016-12-16", - "device": "tablet", - "visits": "1663874" + date: "2016-12-16", + device: "tablet", + visits: "1663874", }, { - "date": "2016-12-17", - "device": "desktop", - "visits": "8866860" + date: "2016-12-17", + device: "desktop", + visits: "8866860", }, { - "date": "2016-12-17", - "device": "mobile", - "visits": "8772502" + date: "2016-12-17", + device: "mobile", + visits: "8772502", }, { - "date": "2016-12-17", - "device": "tablet", - "visits": "1627369" + date: "2016-12-17", + device: "tablet", + visits: "1627369", }, { - "date": "2016-12-18", - "device": "desktop", - "visits": "8105408" + date: "2016-12-18", + device: "desktop", + visits: "8105408", }, { - "date": "2016-12-18", - "device": "mobile", - "visits": "7414904" + date: "2016-12-18", + device: "mobile", + visits: "7414904", }, { - "date": "2016-12-18", - "device": "tablet", - "visits": "1469536" + date: "2016-12-18", + device: "tablet", + visits: "1469536", }, { - "date": "2016-12-19", - "device": "desktop", - "visits": "19220918" + date: "2016-12-19", + device: "desktop", + visits: "19220918", }, { - "date": "2016-12-19", - "device": "mobile", - "visits": "10438620" + date: "2016-12-19", + device: "mobile", + visits: "10438620", }, { - "date": "2016-12-19", - "device": "tablet", - "visits": "1677447" + date: "2016-12-19", + device: "tablet", + visits: "1677447", }, { - "date": "2016-12-20", - "device": "desktop", - "visits": "18241079" + date: "2016-12-20", + device: "desktop", + visits: "18241079", }, { - "date": "2016-12-20", - "device": "mobile", - "visits": "10558487" + date: "2016-12-20", + device: "mobile", + visits: "10558487", }, { - "date": "2016-12-20", - "device": "tablet", - "visits": "1618781" + date: "2016-12-20", + device: "tablet", + visits: "1618781", }, { - "date": "2016-12-21", - "device": "desktop", - "visits": "17147953" + date: "2016-12-21", + device: "desktop", + visits: "17147953", }, { - "date": "2016-12-21", - "device": "mobile", - "visits": "10422959" + date: "2016-12-21", + device: "mobile", + visits: "10422959", }, { - "date": "2016-12-21", - "device": "tablet", - "visits": "1563992" + date: "2016-12-21", + device: "tablet", + visits: "1563992", }, { - "date": "2016-12-22", - "device": "desktop", - "visits": "15503945" + date: "2016-12-22", + device: "desktop", + visits: "15503945", }, { - "date": "2016-12-22", - "device": "mobile", - "visits": "10305992" + date: "2016-12-22", + device: "mobile", + visits: "10305992", }, { - "date": "2016-12-22", - "device": "tablet", - "visits": "1529405" + date: "2016-12-22", + device: "tablet", + visits: "1529405", }, { - "date": "2016-12-23", - "device": "desktop", - "visits": "11361437" + date: "2016-12-23", + device: "desktop", + visits: "11361437", }, { - "date": "2016-12-23", - "device": "mobile", - "visits": "9521278" + date: "2016-12-23", + device: "mobile", + visits: "9521278", }, { - "date": "2016-12-23", - "device": "tablet", - "visits": "1446075" + date: "2016-12-23", + device: "tablet", + visits: "1446075", }, { - "date": "2016-12-24", - "device": "desktop", - "visits": "5600182" + date: "2016-12-24", + device: "desktop", + visits: "5600182", }, { - "date": "2016-12-24", - "device": "mobile", - "visits": "7144987" + date: "2016-12-24", + device: "mobile", + visits: "7144987", }, { - "date": "2016-12-24", - "device": "tablet", - "visits": "1190168" + date: "2016-12-24", + device: "tablet", + visits: "1190168", }, { - "date": "2016-12-25", - "device": "desktop", - "visits": "4408666" + date: "2016-12-25", + device: "desktop", + visits: "4408666", }, { - "date": "2016-12-25", - "device": "mobile", - "visits": "5531137" + date: "2016-12-25", + device: "mobile", + visits: "5531137", }, { - "date": "2016-12-25", - "device": "tablet", - "visits": "1026063" + date: "2016-12-25", + device: "tablet", + visits: "1026063", }, { - "date": "2016-12-26", - "device": "desktop", - "visits": "7825098" + date: "2016-12-26", + device: "desktop", + visits: "7825098", }, { - "date": "2016-12-26", - "device": "mobile", - "visits": "7232890" + date: "2016-12-26", + device: "mobile", + visits: "7232890", }, { - "date": "2016-12-26", - "device": "tablet", - "visits": "1355893" + date: "2016-12-26", + device: "tablet", + visits: "1355893", }, { - "date": "2016-12-27", - "device": "desktop", - "visits": "13935273" + date: "2016-12-27", + device: "desktop", + visits: "13935273", }, { - "date": "2016-12-27", - "device": "mobile", - "visits": "8975892" + date: "2016-12-27", + device: "mobile", + visits: "8975892", }, { - "date": "2016-12-27", - "device": "tablet", - "visits": "1445369" + date: "2016-12-27", + device: "tablet", + visits: "1445369", }, { - "date": "2016-12-28", - "device": "desktop", - "visits": "14480665" + date: "2016-12-28", + device: "desktop", + visits: "14480665", }, { - "date": "2016-12-28", - "device": "mobile", - "visits": "9244411" + date: "2016-12-28", + device: "mobile", + visits: "9244411", }, { - "date": "2016-12-28", - "device": "tablet", - "visits": "1495648" + date: "2016-12-28", + device: "tablet", + visits: "1495648", }, { - "date": "2016-12-29", - "device": "desktop", - "visits": "14178667" + date: "2016-12-29", + device: "desktop", + visits: "14178667", }, { - "date": "2016-12-29", - "device": "mobile", - "visits": "9223986" + date: "2016-12-29", + device: "mobile", + visits: "9223986", }, { - "date": "2016-12-29", - "device": "tablet", - "visits": "1501026" + date: "2016-12-29", + device: "tablet", + visits: "1501026", }, { - "date": "2016-12-30", - "device": "desktop", - "visits": "11547674" + date: "2016-12-30", + device: "desktop", + visits: "11547674", }, { - "date": "2016-12-30", - "device": "mobile", - "visits": "8372061" + date: "2016-12-30", + device: "mobile", + visits: "8372061", }, { - "date": "2016-12-30", - "device": "tablet", - "visits": "1373276" + date: "2016-12-30", + device: "tablet", + visits: "1373276", }, { - "date": "2016-12-31", - "device": "desktop", - "visits": "6126765" + date: "2016-12-31", + device: "desktop", + visits: "6126765", }, { - "date": "2016-12-31", - "device": "mobile", - "visits": "6393735" + date: "2016-12-31", + device: "mobile", + visits: "6393735", }, { - "date": "2016-12-31", - "device": "tablet", - "visits": "1188851" + date: "2016-12-31", + device: "tablet", + visits: "1188851", }, { - "date": "2017-01-01", - "device": "desktop", - "visits": "5717572" + date: "2017-01-01", + device: "desktop", + visits: "5717572", }, { - "date": "2017-01-01", - "device": "mobile", - "visits": "6002253" + date: "2017-01-01", + device: "mobile", + visits: "6002253", }, { - "date": "2017-01-01", - "device": "tablet", - "visits": "1219702" + date: "2017-01-01", + device: "tablet", + visits: "1219702", }, { - "date": "2017-01-02", - "device": "desktop", - "visits": "10414034" + date: "2017-01-02", + device: "desktop", + visits: "10414034", }, { - "date": "2017-01-02", - "device": "mobile", - "visits": "8280913" + date: "2017-01-02", + device: "mobile", + visits: "8280913", }, { - "date": "2017-01-02", - "device": "tablet", - "visits": "1572182" + date: "2017-01-02", + device: "tablet", + visits: "1572182", }, { - "date": "2017-01-03", - "device": "desktop", - "visits": "19074040" + date: "2017-01-03", + device: "desktop", + visits: "19074040", }, { - "date": "2017-01-03", - "device": "mobile", - "visits": "10002388" + date: "2017-01-03", + device: "mobile", + visits: "10002388", }, { - "date": "2017-01-03", - "device": "tablet", - "visits": "1634073" + date: "2017-01-03", + device: "tablet", + visits: "1634073", }, { - "date": "2017-01-04", - "device": "desktop", - "visits": "19474263" + date: "2017-01-04", + device: "desktop", + visits: "19474263", }, { - "date": "2017-01-04", - "device": "mobile", - "visits": "10263370" + date: "2017-01-04", + device: "mobile", + visits: "10263370", }, { - "date": "2017-01-04", - "device": "tablet", - "visits": "1707684" + date: "2017-01-04", + device: "tablet", + visits: "1707684", }, { - "date": "2017-01-05", - "device": "desktop", - "visits": "19466017" + date: "2017-01-05", + device: "desktop", + visits: "19466017", }, { - "date": "2017-01-05", - "device": "mobile", - "visits": "10736442" + date: "2017-01-05", + device: "mobile", + visits: "10736442", }, { - "date": "2017-01-05", - "device": "tablet", - "visits": "1762507" + date: "2017-01-05", + device: "tablet", + visits: "1762507", }, { - "date": "2017-01-06", - "device": "desktop", - "visits": "17268777" + date: "2017-01-06", + device: "desktop", + visits: "17268777", }, { - "date": "2017-01-06", - "device": "mobile", - "visits": "10204089" + date: "2017-01-06", + device: "mobile", + visits: "10204089", }, { - "date": "2017-01-06", - "device": "tablet", - "visits": "1700304" + date: "2017-01-06", + device: "tablet", + visits: "1700304", }, { - "date": "2017-01-07", - "device": "desktop", - "visits": "8771825" + date: "2017-01-07", + device: "desktop", + visits: "8771825", }, { - "date": "2017-01-07", - "device": "mobile", - "visits": "8622569" + date: "2017-01-07", + device: "mobile", + visits: "8622569", }, { - "date": "2017-01-07", - "device": "tablet", - "visits": "1657525" + date: "2017-01-07", + device: "tablet", + visits: "1657525", }, { - "date": "2017-01-08", - "device": "desktop", - "visits": "8468167" + date: "2017-01-08", + device: "desktop", + visits: "8468167", }, { - "date": "2017-01-08", - "device": "mobile", - "visits": "7523797" + date: "2017-01-08", + device: "mobile", + visits: "7523797", }, { - "date": "2017-01-08", - "device": "tablet", - "visits": "1573548" + date: "2017-01-08", + device: "tablet", + visits: "1573548", }, { - "date": "2017-01-09", - "device": "desktop", - "visits": "19946515" + date: "2017-01-09", + device: "desktop", + visits: "19946515", }, { - "date": "2017-01-09", - "device": "mobile", - "visits": "10112103" + date: "2017-01-09", + device: "mobile", + visits: "10112103", }, { - "date": "2017-01-09", - "device": "tablet", - "visits": "1724557" + date: "2017-01-09", + device: "tablet", + visits: "1724557", }, { - "date": "2017-01-10", - "device": "desktop", - "visits": "20321640" + date: "2017-01-10", + device: "desktop", + visits: "20321640", }, { - "date": "2017-01-10", - "device": "mobile", - "visits": "10515776" + date: "2017-01-10", + device: "mobile", + visits: "10515776", }, { - "date": "2017-01-10", - "device": "tablet", - "visits": "1795632" + date: "2017-01-10", + device: "tablet", + visits: "1795632", }, { - "date": "2017-01-11", - "device": "desktop", - "visits": "19671577" + date: "2017-01-11", + device: "desktop", + visits: "19671577", }, { - "date": "2017-01-11", - "device": "mobile", - "visits": "10465313" + date: "2017-01-11", + device: "mobile", + visits: "10465313", }, { - "date": "2017-01-11", - "device": "tablet", - "visits": "1732368" + date: "2017-01-11", + device: "tablet", + visits: "1732368", }, { - "date": "2017-01-12", - "device": "desktop", - "visits": "19589937" + date: "2017-01-12", + device: "desktop", + visits: "19589937", }, { - "date": "2017-01-12", - "device": "mobile", - "visits": "10277052" + date: "2017-01-12", + device: "mobile", + visits: "10277052", }, { - "date": "2017-01-12", - "device": "tablet", - "visits": "1703584" + date: "2017-01-12", + device: "tablet", + visits: "1703584", }, { - "date": "2017-01-13", - "device": "desktop", - "visits": "17146743" + date: "2017-01-13", + device: "desktop", + visits: "17146743", }, { - "date": "2017-01-13", - "device": "mobile", - "visits": "9619211" + date: "2017-01-13", + device: "mobile", + visits: "9619211", }, { - "date": "2017-01-13", - "device": "tablet", - "visits": "1585216" + date: "2017-01-13", + device: "tablet", + visits: "1585216", }, { - "date": "2017-01-14", - "device": "desktop", - "visits": "8330783" + date: "2017-01-14", + device: "desktop", + visits: "8330783", }, { - "date": "2017-01-14", - "device": "mobile", - "visits": "8038168" + date: "2017-01-14", + device: "mobile", + visits: "8038168", }, { - "date": "2017-01-14", - "device": "tablet", - "visits": "1474055" + date: "2017-01-14", + device: "tablet", + visits: "1474055", }, { - "date": "2017-01-15", - "device": "desktop", - "visits": "7940108" + date: "2017-01-15", + device: "desktop", + visits: "7940108", }, { - "date": "2017-01-15", - "device": "mobile", - "visits": "7377663" + date: "2017-01-15", + device: "mobile", + visits: "7377663", }, { - "date": "2017-01-15", - "device": "tablet", - "visits": "1420365" + date: "2017-01-15", + device: "tablet", + visits: "1420365", }, { - "date": "2017-01-16", - "device": "desktop", - "visits": "14829426" + date: "2017-01-16", + device: "desktop", + visits: "14829426", }, { - "date": "2017-01-16", - "device": "mobile", - "visits": "9257283" + date: "2017-01-16", + device: "mobile", + visits: "9257283", }, { - "date": "2017-01-16", - "device": "tablet", - "visits": "1558470" + date: "2017-01-16", + device: "tablet", + visits: "1558470", }, { - "date": "2017-01-17", - "device": "desktop", - "visits": "21076771" + date: "2017-01-17", + device: "desktop", + visits: "21076771", }, { - "date": "2017-01-17", - "device": "mobile", - "visits": "11441390" + date: "2017-01-17", + device: "mobile", + visits: "11441390", }, { - "date": "2017-01-17", - "device": "tablet", - "visits": "1742698" + date: "2017-01-17", + device: "tablet", + visits: "1742698", }, { - "date": "2017-01-18", - "device": "desktop", - "visits": "20446130" + date: "2017-01-18", + device: "desktop", + visits: "20446130", }, { - "date": "2017-01-18", - "device": "mobile", - "visits": "10970693" + date: "2017-01-18", + device: "mobile", + visits: "10970693", }, { - "date": "2017-01-18", - "device": "tablet", - "visits": "1717717" + date: "2017-01-18", + device: "tablet", + visits: "1717717", }, { - "date": "2017-01-19", - "device": "desktop", - "visits": "20157052" + date: "2017-01-19", + device: "desktop", + visits: "20157052", }, { - "date": "2017-01-19", - "device": "mobile", - "visits": "11228989" + date: "2017-01-19", + device: "mobile", + visits: "11228989", }, { - "date": "2017-01-19", - "device": "tablet", - "visits": "1726224" + date: "2017-01-19", + device: "tablet", + visits: "1726224", }, { - "date": "2017-01-20", - "device": "desktop", - "visits": "19344217" + date: "2017-01-20", + device: "desktop", + visits: "19344217", }, { - "date": "2017-01-20", - "device": "mobile", - "visits": "12884804" + date: "2017-01-20", + device: "mobile", + visits: "12884804", }, { - "date": "2017-01-20", - "device": "tablet", - "visits": "1873116" + date: "2017-01-20", + device: "tablet", + visits: "1873116", }, { - "date": "2017-01-21", - "device": "desktop", - "visits": "9950647" + date: "2017-01-21", + device: "desktop", + visits: "9950647", }, { - "date": "2017-01-21", - "device": "mobile", - "visits": "10568161" + date: "2017-01-21", + device: "mobile", + visits: "10568161", }, { - "date": "2017-01-21", - "device": "tablet", - "visits": "1783297" + date: "2017-01-21", + device: "tablet", + visits: "1783297", }, { - "date": "2017-01-22", - "device": "desktop", - "visits": "10151644" + date: "2017-01-22", + device: "desktop", + visits: "10151644", }, { - "date": "2017-01-22", - "device": "mobile", - "visits": "9316374" + date: "2017-01-22", + device: "mobile", + visits: "9316374", }, { - "date": "2017-01-22", - "device": "tablet", - "visits": "1822457" + date: "2017-01-22", + device: "tablet", + visits: "1822457", }, { - "date": "2017-01-23", - "device": "desktop", - "visits": "23257771" + date: "2017-01-23", + device: "desktop", + visits: "23257771", }, { - "date": "2017-01-23", - "device": "mobile", - "visits": "12281874" + date: "2017-01-23", + device: "mobile", + visits: "12281874", }, { - "date": "2017-01-23", - "device": "tablet", - "visits": "1957768" + date: "2017-01-23", + device: "tablet", + visits: "1957768", }, { - "date": "2017-01-24", - "device": "desktop", - "visits": "21802654" + date: "2017-01-24", + device: "desktop", + visits: "21802654", }, { - "date": "2017-01-24", - "device": "mobile", - "visits": "11787571" + date: "2017-01-24", + device: "mobile", + visits: "11787571", }, { - "date": "2017-01-24", - "device": "tablet", - "visits": "1840512" + date: "2017-01-24", + device: "tablet", + visits: "1840512", }, { - "date": "2017-01-25", - "device": "desktop", - "visits": "21217961" + date: "2017-01-25", + device: "desktop", + visits: "21217961", }, { - "date": "2017-01-25", - "device": "mobile", - "visits": "12259488" + date: "2017-01-25", + device: "mobile", + visits: "12259488", }, { - "date": "2017-01-25", - "device": "tablet", - "visits": "1824556" + date: "2017-01-25", + device: "tablet", + visits: "1824556", }, { - "date": "2017-01-26", - "device": "desktop", - "visits": "20151178" + date: "2017-01-26", + device: "desktop", + visits: "20151178", }, { - "date": "2017-01-26", - "device": "mobile", - "visits": "11692776" + date: "2017-01-26", + device: "mobile", + visits: "11692776", }, { - "date": "2017-01-26", - "device": "tablet", - "visits": "1720242" + date: "2017-01-26", + device: "tablet", + visits: "1720242", }, { - "date": "2017-01-27", - "device": "desktop", - "visits": "17657726" + date: "2017-01-27", + device: "desktop", + visits: "17657726", }, { - "date": "2017-01-27", - "device": "mobile", - "visits": "10761667" + date: "2017-01-27", + device: "mobile", + visits: "10761667", }, { - "date": "2017-01-27", - "device": "tablet", - "visits": "1574402" + date: "2017-01-27", + device: "tablet", + visits: "1574402", }, { - "date": "2017-01-28", - "device": "desktop", - "visits": "9175780" + date: "2017-01-28", + device: "desktop", + visits: "9175780", }, { - "date": "2017-01-28", - "device": "mobile", - "visits": "9316210" + date: "2017-01-28", + device: "mobile", + visits: "9316210", }, { - "date": "2017-01-28", - "device": "tablet", - "visits": "1486173" + date: "2017-01-28", + device: "tablet", + visits: "1486173", }, { - "date": "2017-01-29", - "device": "desktop", - "visits": "9761406" + date: "2017-01-29", + device: "desktop", + visits: "9761406", }, { - "date": "2017-01-29", - "device": "mobile", - "visits": "9702597" + date: "2017-01-29", + device: "mobile", + visits: "9702597", }, { - "date": "2017-01-29", - "device": "tablet", - "visits": "1606222" + date: "2017-01-29", + device: "tablet", + visits: "1606222", }, { - "date": "2017-01-30", - "device": "desktop", - "visits": "22638067" + date: "2017-01-30", + device: "desktop", + visits: "22638067", }, { - "date": "2017-01-30", - "device": "mobile", - "visits": "12653369" + date: "2017-01-30", + device: "mobile", + visits: "12653369", }, { - "date": "2017-01-30", - "device": "tablet", - "visits": "1858651" + date: "2017-01-30", + device: "tablet", + visits: "1858651", }, { - "date": "2017-01-31", - "device": "desktop", - "visits": "22251428" + date: "2017-01-31", + device: "desktop", + visits: "22251428", }, { - "date": "2017-01-31", - "device": "mobile", - "visits": "12268125" + date: "2017-01-31", + device: "mobile", + visits: "12268125", }, { - "date": "2017-01-31", - "device": "tablet", - "visits": "1819209" + date: "2017-01-31", + device: "tablet", + visits: "1819209", }, { - "date": "2017-02-01", - "device": "desktop", - "visits": "21087290" + date: "2017-02-01", + device: "desktop", + visits: "21087290", }, { - "date": "2017-02-01", - "device": "mobile", - "visits": "12257163" + date: "2017-02-01", + device: "mobile", + visits: "12257163", }, { - "date": "2017-02-01", - "device": "tablet", - "visits": "1791769" + date: "2017-02-01", + device: "tablet", + visits: "1791769", }, { - "date": "2017-02-02", - "device": "desktop", - "visits": "20524207" + date: "2017-02-02", + device: "desktop", + visits: "20524207", }, { - "date": "2017-02-02", - "device": "mobile", - "visits": "12114547" + date: "2017-02-02", + device: "mobile", + visits: "12114547", }, { - "date": "2017-02-02", - "device": "tablet", - "visits": "1757504" + date: "2017-02-02", + device: "tablet", + visits: "1757504", }, { - "date": "2017-02-03", - "device": "desktop", - "visits": "17997793" + date: "2017-02-03", + device: "desktop", + visits: "17997793", }, { - "date": "2017-02-03", - "device": "mobile", - "visits": "11483512" + date: "2017-02-03", + device: "mobile", + visits: "11483512", }, { - "date": "2017-02-03", - "device": "tablet", - "visits": "1646621" + date: "2017-02-03", + device: "tablet", + visits: "1646621", }, { - "date": "2017-02-04", - "device": "desktop", - "visits": "9313172" + date: "2017-02-04", + device: "desktop", + visits: "9313172", }, { - "date": "2017-02-04", - "device": "mobile", - "visits": "9544262" + date: "2017-02-04", + device: "mobile", + visits: "9544262", }, { - "date": "2017-02-04", - "device": "tablet", - "visits": "1503310" + date: "2017-02-04", + device: "tablet", + visits: "1503310", }, { - "date": "2017-02-05", - "device": "desktop", - "visits": "8833525" + date: "2017-02-05", + device: "desktop", + visits: "8833525", }, { - "date": "2017-02-05", - "device": "mobile", - "visits": "8273273" + date: "2017-02-05", + device: "mobile", + visits: "8273273", }, { - "date": "2017-02-05", - "device": "tablet", - "visits": "1436846" + date: "2017-02-05", + device: "tablet", + visits: "1436846", }, { - "date": "2017-02-06", - "device": "desktop", - "visits": "21775734" + date: "2017-02-06", + device: "desktop", + visits: "21775734", }, { - "date": "2017-02-06", - "device": "mobile", - "visits": "12223955" + date: "2017-02-06", + device: "mobile", + visits: "12223955", }, { - "date": "2017-02-06", - "device": "tablet", - "visits": "1821893" + date: "2017-02-06", + device: "tablet", + visits: "1821893", }, { - "date": "2017-02-07", - "device": "desktop", - "visits": "22100599" + date: "2017-02-07", + device: "desktop", + visits: "22100599", }, { - "date": "2017-02-07", - "device": "mobile", - "visits": "12625240" + date: "2017-02-07", + device: "mobile", + visits: "12625240", }, { - "date": "2017-02-07", - "device": "tablet", - "visits": "1899859" + date: "2017-02-07", + device: "tablet", + visits: "1899859", }, { - "date": "2017-02-08", - "device": "desktop", - "visits": "22031758" + date: "2017-02-08", + device: "desktop", + visits: "22031758", }, { - "date": "2017-02-08", - "device": "mobile", - "visits": "13262193" + date: "2017-02-08", + device: "mobile", + visits: "13262193", }, { - "date": "2017-02-08", - "device": "tablet", - "visits": "1931228" + date: "2017-02-08", + device: "tablet", + visits: "1931228", }, { - "date": "2017-02-09", - "device": "desktop", - "visits": "20575032" + date: "2017-02-09", + device: "desktop", + visits: "20575032", }, { - "date": "2017-02-09", - "device": "mobile", - "visits": "12979335" + date: "2017-02-09", + device: "mobile", + visits: "12979335", }, { - "date": "2017-02-09", - "device": "tablet", - "visits": "1921387" + date: "2017-02-09", + device: "tablet", + visits: "1921387", }, { - "date": "2017-02-10", - "device": "desktop", - "visits": "17711813" + date: "2017-02-10", + device: "desktop", + visits: "17711813", }, { - "date": "2017-02-10", - "device": "mobile", - "visits": "11965905" + date: "2017-02-10", + device: "mobile", + visits: "11965905", }, { - "date": "2017-02-10", - "device": "tablet", - "visits": "1675788" + date: "2017-02-10", + device: "tablet", + visits: "1675788", }, { - "date": "2017-02-11", - "device": "desktop", - "visits": "9097741" + date: "2017-02-11", + device: "desktop", + visits: "9097741", }, { - "date": "2017-02-11", - "device": "mobile", - "visits": "10059393" + date: "2017-02-11", + device: "mobile", + visits: "10059393", }, { - "date": "2017-02-11", - "device": "tablet", - "visits": "1542236" + date: "2017-02-11", + device: "tablet", + visits: "1542236", }, { - "date": "2017-02-12", - "device": "desktop", - "visits": "9652936" + date: "2017-02-12", + device: "desktop", + visits: "9652936", }, { - "date": "2017-02-12", - "device": "mobile", - "visits": "9133410" + date: "2017-02-12", + device: "mobile", + visits: "9133410", }, { - "date": "2017-02-12", - "device": "tablet", - "visits": "1592009" + date: "2017-02-12", + device: "tablet", + visits: "1592009", }, { - "date": "2017-02-13", - "device": "desktop", - "visits": "20780584" + date: "2017-02-13", + device: "desktop", + visits: "20780584", }, { - "date": "2017-02-13", - "device": "mobile", - "visits": "12435261" + date: "2017-02-13", + device: "mobile", + visits: "12435261", }, { - "date": "2017-02-13", - "device": "tablet", - "visits": "1753516" + date: "2017-02-13", + device: "tablet", + visits: "1753516", }, { - "date": "2017-02-14", - "device": "desktop", - "visits": "19207139" + date: "2017-02-14", + device: "desktop", + visits: "19207139", }, { - "date": "2017-02-14", - "device": "mobile", - "visits": "11879814" + date: "2017-02-14", + device: "mobile", + visits: "11879814", }, { - "date": "2017-02-14", - "device": "tablet", - "visits": "1642179" - } + date: "2017-02-14", + device: "tablet", + visits: "1642179", + }, ], - "totals": { - "visits": 2380289500, - "devices": { - "desktop": 1369555309, - "mobile": 868783942, - "tablet": 141950249 - } + totals: { + visits: 2380289500, + devices: { + desktop: 1369555309, + mobile: 868783942, + tablet: 141950249, + }, }, - "taken_at": "2017-02-15T15:44:53.044Z" -} - + taken_at: "2017-02-15T15:44:53.044Z", +}; diff --git a/ua/test/support/mocks/googleapis-analytics.js b/ua/test/support/mocks/googleapis-analytics.js index 535abb4e..5613d5c3 100644 --- a/ua/test/support/mocks/googleapis-analytics.js +++ b/ua/test/support/mocks/googleapis-analytics.js @@ -1,18 +1,18 @@ -const dataFixture = require("../fixtures/data") +const dataFixture = require("../fixtures/data"); const googleAPIsMock = () => { - const data = Object.assign({}, dataFixture) - const realtime = { get: (query, callback) => callback(null, data) } - const ga = { get: (query, callback) => callback(null, data) } + const data = Object.assign({}, dataFixture); + const realtime = { get: (query, callback) => callback(null, data) }; + const ga = { get: (query, callback) => callback(null, data) }; - const analytics = (() => ({ + const analytics = () => ({ data: { realtime: realtime, ga: ga, - } - })) + }, + }); - return { realtime, ga, analytics } -} + return { realtime, ga, analytics }; +}; -module.exports = googleAPIsMock +module.exports = googleAPIsMock; diff --git a/ua/test/support/mocks/googleapis-auth.js b/ua/test/support/mocks/googleapis-auth.js index f6a2bf72..0c7e4411 100644 --- a/ua/test/support/mocks/googleapis-auth.js +++ b/ua/test/support/mocks/googleapis-auth.js @@ -1,11 +1,11 @@ -const dataFixture = require("../fixtures/data") +const dataFixture = require("../fixtures/data"); const googleAPIsMock = () => { function JWT() { - this.initArguments = arguments + this.initArguments = arguments; } - JWT.prototype.authorize = (callback) => callback(null, {}) - return { Auth: { JWT } } -} + JWT.prototype.authorize = (callback) => callback(null, {}); + return { Auth: { JWT } }; +}; -module.exports = googleAPIsMock +module.exports = googleAPIsMock;