diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 127acd6..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,243 +0,0 @@ -version: 2.1 -jobs: - develop_deploy: - docker: - - image: cimg/node:16.19.1-browsers - environment: - POSTGRES_USER: postgres - NODE_ENV: test - - image: circleci/postgres:9.5-alpine - environment: - POSTGRES_DB: analytics-api-test - steps: - - checkout - - - restore_cache: - keys: - - v1-dependencies-{{ checksum "package.json" }} - - v1-dependencies- - - - run: - name: install dependencies - command: npm install - - - save_cache: - paths: - - ./node_modules - key: v1-dependencies-{{ checksum "package.json" }} - - - run: - name: run tests - command: npm test - - - run: - name: Replace Data URL in manifest.yml file when deploying to develop - command: | - sudo apt-get update - sudo apt-get install gettext - sed -i 's@name: analytics-reporter-api@name: analytics-reporter-api-develop@g' manifest.yml - sed -i 's@- analytics-reporter-database@- analytics-reporter-database-develop@g' manifest.yml - mv manifest.yml manifest.yml.src - envsubst < manifest.yml.src > manifest.yml - - - run: - name: Run sed on entrypoint.sh when deploying to develop - command: | - sed -i 's@NEW_RELIC_APP_NAME="analytics-reporter-api"@NEW_RELIC_APP_NAME="analytics-reporter-api-develop"@g' entrypoint.sh - - - run: - name: Delete Config.js and drop sufffix on Config.js.cloudgov - command: | - rm ./src/config.js - mv ./src/config.js.cloudgov ./src/config.js - rm knexfile.js - mv knexfile.js.cloudgov knexfile.js - - - run: - name: Install CF CLI - command: | - 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 - - - run: - name: deploy - command: | - set -e - # Log into cloud.gov - cf api api.fr.cloud.gov - cf login -u $CF_USERNAME_DEV -p $CF_PASSWORD_DEV -o gsa-opp-analytics -s analytics-dev - cat manifest.yml - cf push -f "./manifest.yml" - cf set-env analytics-reporter-api-develop API_DATA_GOV_SECRET "$API_SECRET_LOWER" - cf restage analytics-reporter-api-develop - cf logout - - staging_deploy: - docker: - - image: cimg/node:16.19.1-browsers - environment: - POSTGRES_USER: postgres - NODE_ENV: test - - image: circleci/postgres:9.5-alpine - environment: - POSTGRES_DB: analytics-api-test - steps: - - checkout - - - restore_cache: - keys: - - v1-dependencies-{{ checksum "package.json" }} - - v1-dependencies- - - - run: - name: install dependencies - command: npm install - - - save_cache: - paths: - - ./node_modules - key: v1-dependencies-{{ checksum "package.json" }} - - - run: - name: run tests - command: npm test - - - run: - name: Replace Data URL in manifest.yml file when deploying to staging - command: | - sudo apt-get update - sudo apt-get install gettext - sed -i 's@name: analytics-reporter-api@name: analytics-reporter-api-staging@g' manifest.yml - sed -i 's@- analytics-reporter-database@- analytics-reporter-database-staging@g' manifest.yml - mv manifest.yml manifest.yml.src - envsubst < manifest.yml.src > manifest.yml - - - run: - name: Run sed on entrypoint.sh when deploying to staging - command: | - sed -i 's@NEW_RELIC_APP_NAME="analytics-reporter-api"@NEW_RELIC_APP_NAME="analytics-reporter-api-staging"@g' entrypoint.sh - - - run: - name: Delete Knexfile.js and drop sufffix on Knexfile.js.cloudgov - command: | - rm knexfile.js - mv knexfile.js.cloudgov knexfile.js - rm ./src/config.js - mv ./src/config.js.cloudgov ./src/config.js - - - run: - name: Install CF CLI - command: | - 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 - - - run: - name: deploy - command: | - set -e - # Log into cloud.gov - cf api api.fr.cloud.gov - cf login -u $CF_STAGING_SPACE_DEPLOYER_USERNAME -p $CF_STAGING_SPACE_DEPLOYER_PASSWORD -o gsa-opp-analytics -s analytics-staging - cat manifest.yml - cf push -f "./manifest.yml" - cf set-env analytics-reporter-api-staging API_DATA_GOV_SECRET "$API_SECRET_LOWER" - cf restage analytics-reporter-api-staging - cf logout - - main_deploy: - docker: - - image: cimg/node:16.19.1-browsers - environment: - POSTGRES_USER: postgres - NODE_ENV: test - - image: circleci/postgres:9.5-alpine - environment: - POSTGRES_DB: analytics-api-test - steps: - - checkout - - - restore_cache: - keys: - - v1-dependencies-{{ checksum "package.json" }} - - v1-dependencies- - - - run: - name: install dependencies - command: npm install - - - save_cache: - paths: - - ./node_modules - key: v1-dependencies-{{ checksum "package.json" }} - - - run: - name: run tests - command: npm test - - - run: - name: Replace Data URL in manifest.yml file when deploying to production - command: | - sudo apt-get update - sudo apt-get install gettext - sed -i 's@name: analytics-reporter-api@name: analytics-reporter-api-production@g' manifest.yml - sed -i 's@- analytics-reporter-database@- analytics-reporter-database-production@g' manifest.yml - mv manifest.yml manifest.yml.src - envsubst < manifest.yml.src > manifest.yml - - - run: - name: Run sed on entrypoint.sh when deploying to main - command: | - sed -i 's@NEW_RELIC_APP_NAME="analytics-reporter-api"@NEW_RELIC_APP_NAME="analytics-reporter-api-production"@g' entrypoint.sh - - - run: - name: Delete Knexfile.js and drop sufffix on Knexfile.js.cloudgov - command: | - rm knexfile.js - mv knexfile.js.cloudgov knexfile.js - rm ./src/config.js - mv ./src/config.js.cloudgov ./src/config.js - - - run: - name: Install CF CLI - command: | - 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 - - - run: - name: deploy - command: | - set -e - # Log into cloud.gov - cf api api.fr.cloud.gov - cf login -u $CF_PRODUCTION_SPACE_DEPLOYER_USERNAME -p $CF_PRODUCTION_SPACE_DEPLOYER_PASSWORD -o gsa-opp-analytics -s analytics-prod - cat manifest.yml - cf push -f "./manifest.yml" - cf set-env analytics-reporter-api-production API_DATA_GOV_SECRET "$API_SECRET" - cf restage analytics-reporter-api-production - cf logout - -workflows: - develop_workflow: - jobs: - - develop_deploy: - filters: - branches: - only: - - develop - - staging: - jobs: - - staging_deploy: - filters: - branches: - only: - - stage - - main_workflow: - jobs: - - main_deploy: - filters: - branches: - only: - - master - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b270d20 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,100 @@ +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 dependencies + run: npm ci + - name: lint javascript + run: npm run lint + test: + needs: lint + runs-on: ubuntu-latest + #Spin up postgres as a service, wait till healthy before moving on. 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 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-api/.github/workflows/deploy.yml@develop + with: + APP_NAME: ${{ vars.APP_NAME_DEV }} + DB_NAME: ${{ vars.DB_NAME_DEV }} + NEW_RELIC_APP_NAME: + ORGANIZATION_NAME: gsa-opp-analytics + SPACE_NAME: analytics-dev + secrets: + CF_USERNAME: ${{ secrets.CF_USERNAME_DEV }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD_DEV }} + API_DATA_GOV_SECRET: ${{ secrets.API_DATA_GOV_SECRET_DEV }} + NEW_RELIC_LICENSE_KEY: + deploy_stg: + needs: + - lint + - test + if: github.ref == 'refs/heads/staging' + uses: 18F/analytics-reporter-api/.github/workflows/deploy.yml@develop + with: + APP_NAME: ${{ vars.APP_NAME_STG }} + DB_NAME: ${{ vars.DB_NAME_STG }} + NEW_RELIC_APP_NAME: + ORGANIZATION_NAME: gsa-opp-analytics + SPACE_NAME: analytics-staging + secrets: + CF_USERNAME: ${{ secrets.CF_USERNAME_STG }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD_STG }} + API_DATA_GOV_SECRET: ${{ secrets.API_DATA_GOV_SECRET_STG }} + NEW_RELIC_LICENSE_KEY: + deploy_prd: + needs: + - lint + - test + if: github.ref == 'refs/heads/master' + uses: 18F/analytics-reporter-api/.github/workflows/deploy.yml@develop + with: + APP_NAME: ${{ vars.APP_NAME_PRD }} + DB_NAME: ${{ vars.DB_NAME_PRD }} + NEW_RELIC_APP_NAME: ${{ vars.NEW_RELIC_APP_NAME_PRD }} + ORGANIZATION_NAME: gsa-opp-analytics + SPACE_NAME: analytics-production + secrets: + CF_USERNAME: ${{ secrets.CF_USERNAME_PRD }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD_PRD }} + API_DATA_GOV_SECRET: ${{ secrets.API_DATA_GOV_SECRET_PRD }} + 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 0000000..1260858 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,77 @@ +on: + workflow_call: + inputs: + APP_NAME: + required: true + type: string + DB_NAME: + required: true + type: string + NEW_RELIC_APP_NAME: + type: string + ORGANIZATION_NAME: + required: true + type: string + SPACE_NAME: + required: true + type: string + secrets: + CF_USERNAME: + required: true + CF_PASSWORD: + required: true + API_DATA_GOV_SECRET: + required: true + NEW_RELIC_LICENSE_KEY: + +env: + APP_NAME: ${{ inputs.APP_NAME }} + DB_NAME: ${{ inputs.DB_NAME }} + NEW_RELIC_APP_NAME: ${{ inputs.NEW_RELIC_APP_NAME }} + ORGANIZATION_NAME: ${{ inputs.ORGANIZATION_NAME }} + SPACE_NAME: ${{ inputs.SPACE_NAME }} + CF_USERNAME: ${{ secrets.CF_USERNAME }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + API_DATA_GOV_SECRET: ${{ secrets.API_DATA_GOV_SECRET }} + NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }} + +jobs: + deploy_api: + 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 dependencies + run: npm ci + - name: Replace env vars in manifest.yml file + run: | + sudo apt-get update + sudo apt-get install gettext + mv manifest.yml manifest.yml.src + envsubst < manifest.yml.src > manifest.yml + - name: Replace config.js and knexfile.js with .cloudgov versions of those files + run: | + rm ./src/config.js + mv ./src/config.js.cloudgov ./src/config.js + rm knexfile.js + mv knexfile.js.cloudgov knexfile.js + - name: Install CF CLI + 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: 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 $ORGANIZATION_NAME -s $SPACE_NAME + cat manifest.yml + cf push -f "./manifest.yml" --no-start + cf set-env $APP_NAME API_DATA_GOV_SECRET "$API_DATA_GOV_SECRET" + cf restage $APP_NAME + cf logout diff --git a/.mocharc.yml b/.mocharc.yml new file mode 100644 index 0000000..d050427 --- /dev/null +++ b/.mocharc.yml @@ -0,0 +1,11 @@ +diff: true +extension: ['js'] +package: './package.json' +slow: '75' +spec: + - 'test/**/*.js' +timeout: '2000' +ui: 'bdd' +watch-files: + - 'src/**/*.js' + - 'test/**/*.js' diff --git a/README.md b/README.md index 8593b03..555a8d3 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,38 @@ -[![Code Climate](https://codeclimate.com/github/18F/analytics-reporter-api/badges/gpa.svg)](https://codeclimate.com/github/18F/analytics-reporter-api) [![CircleCI](https://circleci.com/gh/18F/analytics-reporter-api.svg?style=shield)](https://circleci.com/gh/18F/analytics-reporter-api) [![Dependency Status](https://gemnasium.com/badges/github.com/18F/analytics-reporter-api.svg)](https://gemnasium.com/github.com/18F/analytics-reporter-api) - - +[![Code Climate](https://codeclimate.com/github/18F/analytics-reporter-api/badges/gpa.svg)](https://codeclimate.com/github/18F/analytics-reporter-api) [![CircleCI](https://circleci.com/gh/18F/analytics-reporter-api.svg?style=shield)](https://circleci.com/gh/18F/analytics-reporter-api) # Analytics API A system for publishing data retrieved from the Google Analytics API by the [Analytics Reporter](https://github.com/18F/analytics-reporter). -This Analytics API serves data written to a PostgreSQL database by the Analytics Reporter, -in response to HTTP requests. +This Analytics API serves data written to a PostgreSQL database by the Analytics +Reporter, in response to HTTP requests. + +This project's data is provided by the Analytics Reporter using the [Google Analytics Data API v1](https://developers.google.com/analytics/devguides/reporting/data/v1/rest). +The analytics data is processed into a flat data structure by the reporter and +stored in the database which is then served by this API. -# Setup +The project previously used the [Google Analytics Core Reporting API v3](https://developers.google.com/analytics/devguides/reporting/core/v3/) +and the [Google Analytics Real Time API v3](https://developers.google.com/analytics/devguides/reporting/realtime/v3/), +also known as Universal Analytics, which has slightly different data points. + +Analytics API v1 serves the Universal Analytics data and Analytics API v2 serves +the new GA4 data. See [Migrating from API V1 to API V2](#migrating-from-api-v1-to-api-v2) +for more details. The Universal Analytics API will be deprecated on July 1, 2024 +and the Analytics API v1 will no longer receive new data after that date. + +## Setup This Analytics API maintains the schema for the database that the [Analytics Reporter](https://github.com/18F/analytics-reporter) writes to. Thus, the Analytics API must be setup and configured before the Analytics Reporter starts writing data. -First, create the database: +### Prerequistites -```shell -createdb analytics-reporter -``` +* NodeJS > v20.x +* A postgres DB running -````bash -export NODE_ENV=development # developing locally -```` +### Clone the code and install dependencies Once the database is created, clone the app and install the dependencies via NPM. The install script has a postinstall hook that will migrate @@ -44,66 +52,151 @@ npm start The API should now be available at `http://localhost:4444/` -Note that the API will not render any data until -[Analytics Reporter](https://github.com/18F/analytics-reporter) -is configured to write to the same database and run with the `--write-to-database` option. +Note that the API will not render any data because the database is empty. +The [Analytics Reporter](https://github.com/18F/analytics-reporter) +can be configured to write to the same database and run with the +`--write-to-database` option in order to populate some records. + +## Using the API + +Full API docs can be found here: https://open.gsa.gov/api/dap/ + +### Environments + +The base URLs for the 3 API envrionments: + - development: https://api.gsa.gov/analytics/dap/develop/ + - staging: https://api.gsa.gov/analytics/dap/staging/ + - production: https://api.gsa.gov/analytics/dap/ -# Using the API +### Overview The Analytics API exposes 3 API endpoints: -include version in the request, ie `/v2/` -- `/reports/:report_name/data` -- `/agencies/:agency_name/reports/:reportName/data` -- `/domain/:domain/reports/:reportName/data` +- `/v{api_version}/reports/{report_name}/data` +- `/v{api_version}/agencies/{agency_name}/reports/{report_name}/data` +- `/v{api_version}/domain/{domain}/reports/{report_name}/data` -Each endpoint renders a JSON array with the most recent 1000 records the +Each endpoint renders a JSON array with the most recent 1000 records that the Analytics Reporter has generated for the given agency and report. If no records are found, an empty array is returned. Records are sorted according to the associated date. -##### Limit +#### Limit query parameter If a different number of records is desired, the `limit` query parameter can be set to specify the desired number of records. ``` -/reports/realtime/data?limit=500 +/v2/reports/realtime/data?limit=500 ``` The maximum number of records that can be rendered for any given request is 10,000. -##### Page +#### Page query parameter If the desired record does not appear for the current request, the `page` query parameter can be used to get the next series of data points. Since the data is ordered by date, this parameter effectively allows older data to be queried. ``` -/reports/realtime/data?page=2 +/v2/reports/realtime/data?page=2 ``` -# Running the Tests +## Migrating from API V1 to API V2 + +### Background + +Analytics API V1 returns data from Google Analytics V3, also known as Universal +Analytics (UA). + +Google is retiring UA and is encouraging users to move to their new +version Google Analytics V4 (GA4) in 2024. + +Analytics API V2 returns data from GA4. + +### Migration details + +#### Requests + +The Analytics API endpoints are the same between V1 and V2, the only difference +for API requests is the API version string. + +#### Responses + +Response data is slightly different in Analytics API V2. This change is due to +the data provided by Google Analytics. Some data fields were retired in GA4, and +some other useful data fields were added. The changes follow: + +##### Deprecated fields + +- browser_version +- has_social_referral +- exits +- exit_page -The Analytics API application is backed by a test suite that uses -[Mocha](https://mochajs.org/) to run tests. +##### New fields -Before running the test suite, a database for the tests will need to be created. +###### bounce_rate + +The percentage of sessions that were not engaged. GA4 defines engaged as a +session that lasts longer than 10 seconds or has multiple pageviews. + +###### file_name + +The page path of a downloaded file. + +###### language_code + +The ISO639 language setting of the user's device. e.g. 'en-us' + +###### session_default_channel_group + +An enum which describes the session. Possible values: + +'Direct', 'Organic Search', 'Paid Social', 'Organic Social', 'Email', +'Affiliates', 'Referral', 'Paid Search', 'Video', and 'Display' + +## 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 -createdb analytics-api-test +npm run lint:fix ``` -Then the tests can be invoked via NPM. The test script has a pretest hook that -migrates the database. +## Running the unit tests +The unit tests for this repo require a local PostgreSQL database. You can run a +local DB server or create a docker container using the provided test compose +file. (Requires docker and docker-compose to be installed) + +Starting a docker test DB: + +```shell +docker-compose -f docker-compose.test.yml up ``` + +Once you have a PostgreSQL DB running locally, you can run the tests. The test +DB connection in knexfile.js has some default connection config which can be +overridden with environment variables. If using the provided docker-compose DB +then you can avoid setting the connection details. + +Run the tests (pre-test hook runs DB migrations): + +```shell npm test ``` -# Creating a new database migration +## Creating a new database migration If you need to migrate the database, you can create a new migration via `knex`, which will create the migration file for you based in part on the migration name you provide. From the root of this repo, run: ``` `npm bin`/knex migrate:make @@ -111,13 +204,13 @@ If you need to migrate the database, you can create a new migration via `knex`, See [knex documentation](https://knexjs.org/#Installation-migrations) for more details. -# Running database migrations +## Running database migrations -## Locally +### Locally `npm run migrate` -## In production +### In production In production, you can run database migrations via `cf run-task`. As with anything in production, be careful when doing this! First, try checking the current status of migrations using the `migrate:status` command @@ -146,7 +239,8 @@ cf run-task analytics-reporter-api --command "knex migrate:latest" --name run_db ``` See [knex documentation](https://knexjs.org/#Installation-migrations) for more details and options on the `migrate` command. -### Public domain + +## Public domain This project is in the worldwide [public domain](LICENSE.md). As stated in [CONTRIBUTING](CONTRIBUTING.md): diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..e889f41 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,10 @@ +version: "3.0" +services: + db: + image: postgres:16 + environment: + - POSTGRES_DB=analytics_reporter_test + - POSTGRES_USER=analytics + - POSTGRES_PASSWORD=123abc + ports: + - "5432:5432" diff --git a/entrypoint.sh b/entrypoint.sh index b3a796b..a9e69b9 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,5 +1,4 @@ #!/bin/bash -export NEW_RELIC_APP_NAME="analytics-reporter-api" export PATH="$PATH:/home/vcap/deps/0/bin" npm run migrate npm start diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..b31c548 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,3 @@ +const eslintPluginPrettierRecommended = require("eslint-plugin-prettier/recommended"); + +module.exports = [eslintPluginPrettierRecommended]; diff --git a/index.js b/index.js index 249efa9..be16b26 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,11 @@ if (process.env.NEW_RELIC_APP_NAME) { - console.log("Starting New Relic"); - require("newrelic"); + console.log("Starting New Relic"); + require("newrelic"); } -const app = require('./src/app'); -const config = require('./src/config'); -const logger = require('./src/logger'); +const app = require("./src/app"); +const config = require("./src/config"); +const logger = require("./src/logger"); app.listen(config.port, () => { console.log(`Listening on ${config.port}`); diff --git a/knexfile.js b/knexfile.js index 3bb107b..79bc1f1 100644 --- a/knexfile.js +++ b/knexfile.js @@ -1,14 +1,14 @@ module.exports = { development: { - client: 'postgresql', + client: "postgresql", connection: { - user: process.env.POSTGRES_USER || 'postgres', - password: process.env.POSTGRES_PASSWORD || '123abc', - database: process.env.POSTGRES_DATABASE || 'analytics-reporter', - } + user: process.env.POSTGRES_USER || "postgres", + password: process.env.POSTGRES_PASSWORD || "123abc", + database: process.env.POSTGRES_DATABASE || "analytics-reporter", + }, }, production: { - client: 'postgresql', + client: "postgresql", connection: { host: process.env.POSTGRES_HOST, user: process.env.POSTGRES_USER, @@ -17,20 +17,22 @@ module.exports = { }, pool: { min: 2, - max: 10 + max: 10, }, migrations: { - tableName: 'knex_migrations' - } + tableName: "knex_migrations", + }, }, test: { - client: 'postgresql', + client: "postgresql", connection: { - user: process.env.CIRCLECI ? 'postgres' : undefined, - database: 'analytics-api-test' + host: process.env.POSTGRES_HOST || "localhost", + user: process.env.POSTGRES_USER || "analytics", + password: process.env.POSTGRES_PASSWORD || "123abc", + database: process.env.POSTGRES_DATABASE || "analytics_reporter_test", }, migrations: { - tableName: 'knex_migrations' - } - } + tableName: "knex_migrations", + }, + }, }; diff --git a/manifest.yml b/manifest.yml index 426f6fd..d28f994 100644 --- a/manifest.yml +++ b/manifest.yml @@ -1,17 +1,17 @@ applications: -- name: analytics-reporter-api +- name: ${APP_NAME} instances: 1 memory: 128M buildpacks: - nodejs_buildpack command: "chmod +x ./entrypoint.sh && ./entrypoint.sh" services: - - analytics-reporter-database + - ${DB_NAME} stack: cflinuxfs4 env: API_DATA_GOV_SECRET: ${API_DATA_GOV_SECRET} NEW_RELIC_APP_NAME: ${NEW_RELIC_APP_NAME} NEW_RELIC_LICENSE_KEY: ${NEW_RELIC_LICENSE_KEY} - NODE_ENV: production - PGSSLMODE: true + NODE_ENV: production + PGSSLMODE: true diff --git a/migrations/20170308164751_create_analytics_data.js b/migrations/20170308164751_create_analytics_data.js index 12f4c0c..8ae0abf 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 19e4fe2..07ef5ea 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, Promise) { + 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 519eace..78a2144 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, 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.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, 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", + ); + }); }; diff --git a/migrations/20210706213753_add_date_id_multi_col_index.js b/migrations/20210706213753_add_date_id_multi_col_index.js index e4dd203..891ac52 100644 --- a/migrations/20210706213753_add_date_id_multi_col_index.js +++ b/migrations/20210706213753_add_date_id_multi_col_index.js @@ -1,10 +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.down = function(knex, Promise) { - return knex.schema.table("analytics_data", table => { - table.dropIndex("analytics_data_date_desc_id_asc") - }) - }; - \ No newline at end of file +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"); + }); +}; diff --git a/migrations/20231218165411_create_analytics_data_ga4.js b/migrations/20231218165411_create_analytics_data_ga4.js index c2f5a3a..2da7f16 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 def01fd..cca3ced 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 59279a0..dc7b770 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 c162fdb..f9d5048 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/package-lock.json b/package-lock.json index 05d25f3..c21180f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,11 +20,9 @@ }, "devDependencies": { "chai": "^4.3.10", - "eslint": "^8.52.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-plugin-import": "^2.29.0", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "extend": ">= 3.0.2", "mocha": "^10.2.0", "nodemon": "^3.0.1", @@ -655,18 +653,6 @@ "tslib": "^2.3.1" } }, - "node_modules/@babel/runtime": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", - "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@colors/colors": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", @@ -828,13 +814,13 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "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.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -855,9 +841,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "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/@newrelic/aws-sdk": { @@ -919,21 +905,6 @@ "node": ">= 6" } }, - "node_modules/@newrelic/native-metrics/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "optional": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@newrelic/security-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@newrelic/security-agent/-/security-agent-0.6.0.tgz", @@ -964,21 +935,6 @@ "ws": "^8.14.2" } }, - "node_modules/@newrelic/security-agent/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "optional": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@newrelic/superagent": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/@newrelic/superagent/-/superagent-7.0.1.tgz", @@ -1023,6 +979,18 @@ "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", @@ -1790,12 +1758,6 @@ "@types/node": "*" } }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true - }, "node_modules/@types/node": { "version": "20.10.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", @@ -1956,141 +1918,11 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "devOptional": true }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, - "node_modules/array-includes": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", - "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", - "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", - "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -2106,53 +1938,17 @@ "node": "*" } }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true - }, "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, - "node_modules/asynciterator.prototype": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", - "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.3" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "devOptional": true }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", - "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/axios": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.4.tgz", @@ -2164,15 +1960,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/axobject-query": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", - "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2558,12 +2345,6 @@ "typedarray": "^0.0.6" } }, - "node_modules/confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "dev": true - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -2622,12 +2403,6 @@ "node": ">= 8" } }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true - }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -2696,23 +2471,6 @@ "node": ">= 0.4" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2730,15 +2488,6 @@ "node": ">= 0.8" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -2784,12 +2533,6 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", @@ -2803,121 +2546,6 @@ "node": ">= 0.8" } }, - "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", - "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", - "dev": true, - "dependencies": { - "asynciterator.prototype": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.1", - "es-set-tostringtag": "^2.0.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.2.1", - "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.0.1" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2998,266 +2626,68 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-config-airbnb": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", - "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", + "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, - "dependencies": { - "eslint-config-airbnb-base": "^15.0.0", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5" - }, - "engines": { - "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" + "bin": { + "eslint-config-prettier": "bin/cli.js" }, "peerDependencies": { - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.3", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-react": "^7.28.0", - "eslint-plugin-react-hooks": "^4.3.0" + "eslint": ">=7.0.0" } }, - "node_modules/eslint-config-airbnb-base": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", - "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "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": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5", - "semver": "^6.3.0" + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^14.18.0 || >=16.0.0" }, - "peerDependencies": { - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.2" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", - "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", - "dev": true, - "dependencies": { - "debug": "^3.2.7" + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" }, - "engines": { - "node": ">=4" + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" }, "peerDependenciesMeta": { - "eslint": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { "optional": true } } }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "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": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", - "semver": "^6.3.1", - "tsconfig-paths": "^3.15.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=4" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", - "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.23.2", - "aria-query": "^5.3.0", - "array-includes": "^3.1.7", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "=4.7.0", - "axobject-query": "^3.2.1", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.15", - "hasown": "^2.0.0", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.33.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", - "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.12", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.8" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", + "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" @@ -3503,6 +2933,12 @@ "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", @@ -3544,9 +2980,9 @@ } }, "node_modules/fastq": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", - "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "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" @@ -3700,15 +3136,6 @@ } } }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.3" - } - }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -3796,33 +3223,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -3872,22 +3272,6 @@ "node": ">=4" } }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/getopts": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", @@ -3940,21 +3324,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -3978,15 +3347,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4028,21 +3388,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -4219,9 +3564,9 @@ } }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "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" @@ -4285,20 +3630,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.2", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/interpret": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", @@ -4315,52 +3646,11 @@ "node": ">= 0.10" } }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, - "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -4373,34 +3663,6 @@ "node": ">=8" } }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -4412,21 +3674,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -4445,18 +3692,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -4466,21 +3701,6 @@ "node": ">=8" } }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4502,27 +3722,6 @@ "node": ">=6.0" } }, - "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4532,21 +3731,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-object": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", @@ -4574,43 +3758,6 @@ "node": ">=8" } }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -4622,51 +3769,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, - "dependencies": { - "which-typed-array": "^1.1.11" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -4679,71 +3781,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": 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/iterator.prototype": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", - "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" - } - }, - "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", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4789,18 +3832,6 @@ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "optional": true }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -4819,21 +3850,6 @@ "node": "*" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", @@ -4911,24 +3927,6 @@ "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, - "node_modules/language-subtag-registry": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", - "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", - "dev": true - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", - "dev": true, - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5033,18 +4031,6 @@ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", "optional": true }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -5135,15 +4121,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/mocha": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", @@ -5312,21 +4289,6 @@ "@prisma/prisma-fmt-wasm": "^4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085" } }, - "node_modules/newrelic/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "optional": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/nise": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", @@ -5424,21 +4386,6 @@ "node": ">=4" } }, - "node_modules/nodemon/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/nodemon/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -5475,119 +4422,10 @@ "node": ">=0.10.0" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", - "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", - "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", - "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" - } - }, - "node_modules/object.hasown": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", - "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", - "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5883,6 +4721,34 @@ "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", @@ -5910,17 +4776,6 @@ "asap": "~2.0.6" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "node_modules/protobufjs": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", @@ -6054,12 +4909,6 @@ "node": ">= 0.8" } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -6096,49 +4945,6 @@ "node": ">= 10.13.0" } }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", - "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/request-ip": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz", @@ -6253,24 +5059,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6290,20 +5078,6 @@ } ] }, - "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safe-stable-stringify": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", @@ -6318,12 +5092,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "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, + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/send": { @@ -6404,20 +5184,6 @@ "node": ">= 0.4" } }, - "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -6477,21 +5243,6 @@ "node": ">=10" } }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/sinon": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", @@ -6589,71 +5340,6 @@ "integrity": "sha512-n69H31OnxSGSZyZbgBlvYIXlrMhJQ0dQAX1js1QDhpaUH6zmU3QYlj07bCwCNlPOu3oRXIubGPl2gDGnHsiCqg==", "optional": true }, - "node_modules/string.prototype.matchall": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", - "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "regexp.prototype.flags": "^1.5.0", - "set-function-name": "^2.0.0", - "side-channel": "^1.0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -6666,15 +5352,6 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6726,21 +5403,6 @@ "node": ">=4.0.0" } }, - "node_modules/superagent/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/supertest": { "version": "6.3.3", "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz", @@ -6813,6 +5475,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", @@ -6973,23 +5651,11 @@ "node": ">= 14.0.0" } }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "optional": true + "devOptional": true }, "node_modules/type-check": { "version": "0.4.0", @@ -7035,92 +5701,12 @@ "node": ">= 0.6" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "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/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -7229,82 +5815,6 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", - "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", - "dev": true, - "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", - "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", - "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dev": true, - "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/winston": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz", diff --git a/package.json b/package.json index 674cd78..9cc6c7e 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,10 @@ "dev": "nodemon src/index.js", "postinstall": "", "pretest": "NODE_ENV=test npm run migrate", - "test": "NODE_ENV=test `npm bin`/mocha test/**/*.test.js --exit", + "test": "NODE_ENV=test mocha", "migrate": "knex migrate:latest", - "lint": "`npm bin`/eslint src/**.js test/**.js *.js" + "lint": "eslint .", + "lint:fix": "eslint . --fix" }, "repository": { "type": "git", @@ -24,11 +25,9 @@ "homepage": "https://github.com/18F/analytics-reporter-api#readme", "devDependencies": { "chai": "^4.3.10", - "eslint": "^8.52.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-plugin-import": "^2.29.0", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "extend": ">= 3.0.2", "mocha": "^10.2.0", "nodemon": "^3.0.1", diff --git a/src/api-data-gov-filter.js b/src/api-data-gov-filter.js index 3c534c3..dda9b52 100644 --- a/src/api-data-gov-filter.js +++ b/src/api-data-gov-filter.js @@ -1,13 +1,15 @@ -const config = require('./config'); +const config = require("./config"); const apiDataGovFilter = (req, res, next) => { - if (!config.api_data_gov_secret || req.path === '/') { + if (!config.api_data_gov_secret || req.path === "/") { return next(); - } else if (req.headers['api-data-gov-secret'] !== config.api_data_gov_secret) { + } else if ( + req.headers["api-data-gov-secret"] !== config.api_data_gov_secret + ) { res.status(403); return res.json({ - message: 'Unauthorized. See https://analytics.usa.gov/developer', - status: 403 + message: "Unauthorized. See https://analytics.usa.gov/developer", + status: 403, }); } return next(); diff --git a/src/app.js b/src/app.js index 0e20e05..89576f0 100644 --- a/src/app.js +++ b/src/app.js @@ -1,13 +1,13 @@ -const express = require('express'); -const apiDataGovFilter = require('./api-data-gov-filter'); -const db = require('./db'); -const logger = require('./logger'); +const express = require("express"); +const apiDataGovFilter = require("./api-data-gov-filter"); +const db = require("./db"); +const logger = require("./logger"); const router = express.Router(); -const routesVersioning = require('express-routes-versioning')(); +const routesVersioning = require("express-routes-versioning")(); const app = express(); -if (process.env.NODE_ENV != 'test') { +if (process.env.NODE_ENV != "test") { app.use(logger); } app.use(apiDataGovFilter); @@ -21,99 +21,128 @@ const formatDateForDataPoint = (dataPoint) => { }; const acceptableDomainReports = [ - 'site', - 'domain', - 'download', - 'second-level-domain' + "site", + "domain", + "download", + "second-level-domain", ]; const checkDomainFilter = (req, res) => { - if (acceptableDomainReports.includes(req.params.reportName) && - req.params.domain) { + if ( + acceptableDomainReports.includes(req.params.reportName) && + req.params.domain + ) { return fetchData(req, res); } - const tryReportText = acceptableDomainReports.join(', '); + const tryReportText = acceptableDomainReports.join(", "); res.status(400); return res.json({ message: `You are requesting a report that cannot be filtered on domain. Please try one of the following reports: ${tryReportText}.`, - status: 400 + status: 400, }); }; const filterDownloadResponse = (response, params) => { - if (params.domain && params.reportName === 'download') { - return response.filter(entry => entry.page.includes(params.domain)); + if (params.domain && params.reportName === "download") { + return response.filter((entry) => entry.page.includes(params.domain)); } return response; }; const fetchData = (req, res) => { const params = Object.assign(req.query, req.params); - db.query(params).then(result => { - const response = result.map(dataPoint => Object.assign( - { - notice: req.version === '1.1' ? 'v1 is being deprecated. Use v2 instead. See https://analytics.usa.gov/developer' : undefined, - id: dataPoint.id, - date: formatDateForDataPoint(dataPoint), - report_name: dataPoint.report_name, - report_agency: dataPoint.report_agency, - }, dataPoint.data)); - - const filteredResponse = filterDownloadResponse(response, params); - res.json(filteredResponse); - - }).catch(err => { - console.error('Unexpected Error:', err); - res.status(400); - return res.json({ - message: 'An error occurred. Please check the application logs.', - status: 400 + db.query(params) + .then((result) => { + const response = result.map((dataPoint) => + Object.assign( + { + notice: + req.version === "1.1" + ? "v1 is being deprecated. Use v2 instead. See https://analytics.usa.gov/developer" + : undefined, + id: dataPoint.id, + date: formatDateForDataPoint(dataPoint), + report_name: dataPoint.report_name, + report_agency: dataPoint.report_agency, + }, + dataPoint.data, + ), + ); + + const filteredResponse = filterDownloadResponse(response, params); + res.json(filteredResponse); + }) + .catch((err) => { + console.error("Unexpected Error:", err); + res.status(400); + return res.json({ + message: "An error occurred. Please check the application logs.", + status: 400, + }); }); - }); }; -app.get('/', (req, res) => { +app.get("/", (req, res) => { res.json({ - current_time: new Date() + current_time: new Date(), }); }); // middleware -router.use('/v:version/', function(req, res, next) { - const version = req.params.version; - req.version = version - next(); +router.use("/v:version/", function (req, res, next) { + const version = req.params.version; + req.version = version; + next(); }); -router.get('/v:version/reports/:reportName/data', -routesVersioning({ - "1.1.0": respondV1, // legacy - "~2.0.0": fetchData, -}, NoMatchFoundCallback)); - -router.get('/v:version/domain/:domain/reports/:reportName/data', -routesVersioning({ - "1.1.0": respondDomainV1, // legacy - "~2.0.0": checkDomainFilter, -}, NoMatchFoundCallback)); - -router.get('/v:version/agencies/:reportAgency/reports/:reportName/data', -routesVersioning({ - "1.1.0": respondV1, // legacy - "~2.0.0": fetchData, -}, NoMatchFoundCallback)); +router.get( + "/v:version/reports/:reportName/data", + routesVersioning( + { + "1.1.0": respondV1, // legacy + "~2.0.0": fetchData, + }, + NoMatchFoundCallback, + ), +); + +router.get( + "/v:version/domain/:domain/reports/:reportName/data", + routesVersioning( + { + "1.1.0": respondDomainV1, // legacy + "~2.0.0": checkDomainFilter, + }, + NoMatchFoundCallback, + ), +); + +router.get( + "/v:version/agencies/:reportAgency/reports/:reportName/data", + routesVersioning( + { + "1.1.0": respondV1, // legacy + "~2.0.0": fetchData, + }, + NoMatchFoundCallback, + ), +); function NoMatchFoundCallback(req, res) { - res.status(404).json("Version not found. Visit https://analytics.usa.gov/developer for information on the latest supported version."); + res + .status(404) + .json( + "Version not found. Visit https://analytics.usa.gov/developer for information on the latest supported version.", + ); } // v1 function respondV1(req, res) { - return fetchData(req, res) + return fetchData(req, res); } function respondDomainV1(req, res) { - return checkDomainFilter(req, res) + return checkDomainFilter(req, res); } module.exports = app; diff --git a/src/config.js b/src/config.js index ed7cca3..ee32564 100644 --- a/src/config.js +++ b/src/config.js @@ -1,8 +1,8 @@ -const knexfile = require('../knexfile'); +const knexfile = require("../knexfile"); module.exports = { api_data_gov_secret: process.env.API_DATA_GOV_SECRET, port: process.env.PORT || 4444, - postgres: knexfile[process.env.NODE_ENV || 'development'].connection, - log_level: process.env.LOG_LEVEL || 'info' + postgres: knexfile[process.env.NODE_ENV || "development"].connection, + log_level: process.env.LOG_LEVEL || "info", }; diff --git a/src/db.js b/src/db.js index 948307c..44be3c5 100644 --- a/src/db.js +++ b/src/db.js @@ -1,8 +1,8 @@ -const knex = require('knex'); +const knex = require("knex"); -const config = require('./config'); +const config = require("./config"); -const db = knex({ client: 'pg', connection: config.postgres }); +const db = knex({ client: "pg", connection: config.postgres }); const parseLimitParam = (limitParam) => { const limit = parseInt(limitParam, 10); @@ -33,30 +33,40 @@ const buildTimeQuery = (before, after) => { return [true]; }; -const queryDomain = (domain, reportName, limitParam, pageParam, before, after, dbTable) => { +const queryDomain = ( + domain, + reportName, + limitParam, + pageParam, + before, + after, + dbTable, +) => { const timeQuery = buildTimeQuery(before, after); - return db(dbTable) - .where({ report_name: reportName }) - .whereRaw('data->> \'domain\' = ?', [domain]) - .whereRaw(...timeQuery) - // Using `orderByRaw` in order to specifcy NULLS LAST, see: - // https://github.com/knex/knex/issues/282 - .orderByRaw('date desc NULLS LAST') - // Previously, this was ordered by data-->total_events and data-->visits. Those queries - // were very slow, and from what I can tell, it's not possible to add the proper multi-field - // index on (date, data-->total_events, data-->visits) to speed up the queries, because `data` - // is a JSON field. See this (rather wordy, sorry) thread for more details: - // https://github.com/18F/analytics-reporter-api/issues/161#issuecomment-874860764 - // - // Ordering by `id` here does _not_ guarantee ordering based on total_events or visits. However, - // the order in which data is inserted into the table (by code in the analytics-reporter repo, which - // pulls from Google Analytics) happens to be in order by visits or total_events, so ordering by - // IDs may in practice keep the same ordering as before - but it would be best not to rely on this. - // A longer term fix would be to move the total_events and visits fields to their own columns. - .orderBy('id', 'asc') - .limit(limitParam) - .offset((pageParam - 1) * limitParam); + return ( + db(dbTable) + .where({ report_name: reportName }) + .whereRaw("data->> 'domain' = ?", [domain]) + .whereRaw(...timeQuery) + // Using `orderByRaw` in order to specifcy NULLS LAST, see: + // https://github.com/knex/knex/issues/282 + .orderByRaw("date desc NULLS LAST") + // Previously, this was ordered by data-->total_events and data-->visits. Those queries + // were very slow, and from what I can tell, it's not possible to add the proper multi-field + // index on (date, data-->total_events, data-->visits) to speed up the queries, because `data` + // is a JSON field. See this (rather wordy, sorry) thread for more details: + // https://github.com/18F/analytics-reporter-api/issues/161#issuecomment-874860764 + // + // Ordering by `id` here does _not_ guarantee ordering based on total_events or visits. However, + // the order in which data is inserted into the table (by code in the analytics-reporter repo, which + // pulls from Google Analytics) happens to be in order by visits or total_events, so ordering by + // IDs may in practice keep the same ordering as before - but it would be best not to rely on this. + // A longer term fix would be to move the total_events and visits fields to their own columns. + .orderBy("id", "asc") + .limit(limitParam) + .offset((pageParam - 1) * limitParam) + ); }; const query = ({ @@ -67,39 +77,52 @@ const query = ({ domain = null, after = null, before = null, - version - }) => { + version, +}) => { // we have different tables for new ga4 // TODO: once UA has sunset we can remove this - const dbTable = version === '1.1' ? "analytics_data" : "analytics_data_ga4" + const dbTable = version === "1.1" ? "analytics_data" : "analytics_data_ga4"; const limitParam = parseLimitParam(limit); const pageParam = parsePageParam(page); - if (domain && reportName !== 'download') { - return queryDomain(domain, reportName, limitParam, pageParam, before, after, dbTable); + if (domain && reportName !== "download") { + return queryDomain( + domain, + reportName, + limitParam, + pageParam, + before, + after, + dbTable, + ); } - const recordQuery = Object.assign({ report_name: reportName, report_agency: reportAgency }); + const recordQuery = Object.assign({ + report_name: reportName, + report_agency: reportAgency, + }); const timeQuery = buildTimeQuery(before, after); - return db(dbTable) - .where(recordQuery) - .whereRaw(...timeQuery) - // Using `orderByRaw` in order to specifcy NULLS LAST, see: - // https://github.com/knex/knex/issues/282 - .orderByRaw('date desc NULLS LAST') - // Previously, this was ordered by data-->total_events and data-->visits. Those queries - // were very slow, and from what I can tell, it's not possible to add the proper multi-field - // index on (date, data-->total_events, data-->visits) to speed up the queries, because `data` - // is a JSON field. See this (rather wordy, sorry) thread for more details: - // https://github.com/18F/analytics-reporter-api/issues/161#issuecomment-874860764 - // - // Ordering by `id` here does _not_ guarantee ordering based on total_events or visits. However, - // the order in which data is inserted into the table (by code in the analytics-reporter repo, which - // pulls from Google Analytics) happens to be in order by visits or total_events, so ordering by - // IDs may in practice keep the same ordering as before - but it would be best not to rely on this. - // A longer term fix would be to move the total_events and visits fields to their own columns. - .orderBy('id', 'asc') - .limit(limitParam) - .offset((pageParam - 1) * limitParam); + return ( + db(dbTable) + .where(recordQuery) + .whereRaw(...timeQuery) + // Using `orderByRaw` in order to specifcy NULLS LAST, see: + // https://github.com/knex/knex/issues/282 + .orderByRaw("date desc NULLS LAST") + // Previously, this was ordered by data-->total_events and data-->visits. Those queries + // were very slow, and from what I can tell, it's not possible to add the proper multi-field + // index on (date, data-->total_events, data-->visits) to speed up the queries, because `data` + // is a JSON field. See this (rather wordy, sorry) thread for more details: + // https://github.com/18F/analytics-reporter-api/issues/161#issuecomment-874860764 + // + // Ordering by `id` here does _not_ guarantee ordering based on total_events or visits. However, + // the order in which data is inserted into the table (by code in the analytics-reporter repo, which + // pulls from Google Analytics) happens to be in order by visits or total_events, so ordering by + // IDs may in practice keep the same ordering as before - but it would be best not to rely on this. + // A longer term fix would be to move the total_events and visits fields to their own columns. + .orderBy("id", "asc") + .limit(limitParam) + .offset((pageParam - 1) * limitParam) + ); }; -module.exports = { query, queryDomain, buildTimeQuery }; +module.exports = { query, queryDomain, buildTimeQuery, dbClient: db }; diff --git a/src/logger.js b/src/logger.js index 0e3ac21..86416ff 100644 --- a/src/logger.js +++ b/src/logger.js @@ -1,11 +1,9 @@ -const expressWinston = require('express-winston'); -const winston = require('winston'); -const config = require('./config'); +const expressWinston = require("express-winston"); +const winston = require("winston"); +const config = require("./config"); const logger = expressWinston.logger({ - transports: [ - new winston.transports.Console() - ], + transports: [new winston.transports.Console()], expressFormat: true, colorize: true, }); diff --git a/test/.eslintrc.yml b/test/.eslintrc.yml deleted file mode 100644 index 427fd2b..0000000 --- a/test/.eslintrc.yml +++ /dev/null @@ -1,6 +0,0 @@ -extends: -- "airbnb/legacy" -parser: "babel-eslint" -rules: - no-undef: off - no-unused-expressions: off diff --git a/test/api-data-gov-filter.test.js b/test/api-data-gov-filter.test.js index a8701ba..6dde5c0 100644 --- a/test/api-data-gov-filter.test.js +++ b/test/api-data-gov-filter.test.js @@ -1,10 +1,10 @@ -const expect = require('chai').expect; -const proxyquire = require('proxyquire'); -const sinon = require('sinon'); +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const sinon = require("sinon"); proxyquire.noCallThru(); -describe('apiDataGovFilter', () => { +describe("apiDataGovFilter", () => { let apiDataGovFilter; let config; let req; @@ -12,77 +12,77 @@ describe('apiDataGovFilter', () => { let next; beforeEach(() => { - config = { api_data_gov_secret: '123abc' }; - apiDataGovFilter = proxyquire('../src/api-data-gov-filter', { - './config': config + config = { api_data_gov_secret: "123abc" }; + apiDataGovFilter = proxyquire("../src/api-data-gov-filter", { + "./config": config, }); req = {}; res = { status: sinon.spy(), - json: sinon.spy() + json: sinon.spy(), }; next = sinon.spy(); }); - context('with a correct api.data.gov secret', () => { + context("with a correct api.data.gov secret", () => { beforeEach(() => { - req.headers = { 'api-data-gov-secret': '123abc' }; + req.headers = { "api-data-gov-secret": "123abc" }; }); - it('should allow requests to the root url', () => { - req.path = '/'; + it("should allow requests to the root url", () => { + req.path = "/"; apiDataGovFilter(req, res, next); expect(next.calledOnce).to.be.true; }); - it('should allow API requests', () => { - req.path = '/reports/site/data?limit=100'; + it("should allow API requests", () => { + req.path = "/reports/site/data?limit=100"; apiDataGovFilter(req, res, next); expect(next.calledOnce).to.be.true; }); }); - context('with an incorrect api.data.gov secret', () => { + context("with an incorrect api.data.gov secret", () => { beforeEach(() => { - req.headers = { 'api-data-gov-secret': '456def' }; + req.headers = { "api-data-gov-secret": "456def" }; }); - it('should allow requests to the root url', () => { - req.path = '/'; + it("should allow requests to the root url", () => { + req.path = "/"; apiDataGovFilter(req, res, next); expect(next.calledOnce).to.be.true; }); - it('should disallow API requests', () => { - req.path = '/reports/site/data?limit=100'; + it("should disallow API requests", () => { + req.path = "/reports/site/data?limit=100"; apiDataGovFilter(req, res, next); expect(next.calledOnce).to.be.false; expect(res.status.firstCall.args[0]).to.equal(403); expect(res.json.firstCall.args[0]).to.deep.equal({ - message: 'Unauthorized. See https://analytics.usa.gov/developer', - status: 403 + message: "Unauthorized. See https://analytics.usa.gov/developer", + status: 403, }); }); }); - context('without an api.data.gov secret', () => { + context("without an api.data.gov secret", () => { beforeEach(() => { delete config.api_data_gov_secret; req.headers = {}; }); - it('should allow requests to the root url', () => { - req.path = '/'; + it("should allow requests to the root url", () => { + req.path = "/"; apiDataGovFilter(req, res, next); expect(next.calledOnce).to.be.true; }); - it('should allow API requests', () => { - req.path = '/reports/site/data?limit=100'; + it("should allow API requests", () => { + req.path = "/reports/site/data?limit=100"; apiDataGovFilter(req, res, next); expect(next.calledOnce).to.be.true; diff --git a/test/app.test.js b/test/app.test.js index ae5fbf5..8de5b26 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -1,69 +1,71 @@ -const logger = require('../src/logger'); +const logger = require("../src/logger"); -logger.level = 'error'; +logger.level = "error"; -const expect = require('chai').expect; -const proxyquire = require('proxyquire'); -const request = require('supertest-as-promised'); +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const request = require("supertest-as-promised"); const db = {}; -const noticeValue = 'v1 is being deprecated. Use v2 instead. See https://analytics.usa.gov/developer' +const noticeValue = + "v1 is being deprecated. Use v2 instead. See https://analytics.usa.gov/developer"; -const app = proxyquire('../src/app', { - './db': db +const app = proxyquire("../src/app", { + "./db": db, }); const handleIfRouteNotice = (route, arr) => { - if (route === 'v1.1') { - return arr.map(object => { - return {...object, notice: noticeValue} + if (route === "v1.1") { + return arr.map((object) => { + return { ...object, notice: noticeValue }; }); } - return arr -} + return arr; +}; -const routes =[ 'v1.1', 'v2' ] - -routes.forEach(route => { +const routes = ["v1.1", "v2"]; +routes.forEach((route) => { describe(`app with ${route}`, () => { beforeEach(() => { db.query = () => Promise.resolve(); }); - it('should pass params from the url to db.query and render the result', done => { + it("should pass params from the url to db.query and render the result", (done) => { db.query = (params) => { - expect(params.reportAgency).to.equal('fake-agency'); - expect(params.reportName).to.equal('fake-report'); + expect(params.reportAgency).to.equal("fake-agency"); + expect(params.reportName).to.equal("fake-report"); const arr = handleIfRouteNotice(route, [ - { id: 1, date: new Date('2017-01-01')}, - { id: 2, date: new Date('2017-01-02')} - ]) - return Promise.resolve(arr) + { id: 1, date: new Date("2017-01-01") }, + { id: 2, date: new Date("2017-01-02") }, + ]); + return Promise.resolve(arr); }; const dataRequest = request(app) .get(`/${route}/agencies/fake-agency/reports/fake-report/data`) .expect(200); - dataRequest.then(response => { - const arr = handleIfRouteNotice(route, [ - { id: 1, date: '2017-01-01'}, - { id: 2, date: '2017-01-02'} - ]) - expect(response.body).to.deep.equal(arr); - done(); - }).catch(done); + dataRequest + .then((response) => { + const arr = handleIfRouteNotice(route, [ + { id: 1, date: "2017-01-01" }, + { id: 2, date: "2017-01-02" }, + ]); + expect(response.body).to.deep.equal(arr); + done(); + }) + .catch(done); }); - it('should not pass the agency param if the request does not specify an agency', done => { + it("should not pass the agency param if the request does not specify an agency", (done) => { db.query = (params) => { expect(params.reportAgency).to.be.undefined; - expect(params.reportName).to.equal('fake-report'); + expect(params.reportName).to.equal("fake-report"); const arr = handleIfRouteNotice(route, [ - { id: 1, date: new Date('2017-01-01')}, - { id: 2, date: new Date('2017-01-02')} - ]) + { id: 1, date: new Date("2017-01-01") }, + { id: 2, date: new Date("2017-01-02") }, + ]); return Promise.resolve(arr); }; @@ -71,25 +73,27 @@ routes.forEach(route => { .get(`/${route}/reports/fake-report/data`) .expect(200); - dataRequest.then(response => { - const arr = handleIfRouteNotice(route, [ - { id: 1, date: '2017-01-01'}, - { id: 2, date: '2017-01-02'} - ]) - expect(response.body).to.deep.equal(arr); - done(); - }).catch(done); + dataRequest + .then((response) => { + const arr = handleIfRouteNotice(route, [ + { id: 1, date: "2017-01-01" }, + { id: 2, date: "2017-01-02" }, + ]); + expect(response.body).to.deep.equal(arr); + done(); + }) + .catch(done); }); - it('should merge the params in the url with query params', done => { + it("should merge the params in the url with query params", (done) => { db.query = (params) => { - expect(params.reportAgency).to.equal('fake-agency'); - expect(params.reportName).to.equal('fake-report'); - expect(params.limit).to.equal('50'); + expect(params.reportAgency).to.equal("fake-agency"); + expect(params.reportName).to.equal("fake-report"); + expect(params.limit).to.equal("50"); const arr = handleIfRouteNotice(route, [ - { id: 1, date: new Date('2017-01-01')}, - { id: 2, date: new Date('2017-01-02')} - ]) + { id: 1, date: new Date("2017-01-01") }, + { id: 2, date: new Date("2017-01-02") }, + ]); return Promise.resolve(arr); }; @@ -97,39 +101,52 @@ routes.forEach(route => { .get(`/${route}/agencies/fake-agency/reports/fake-report/data?limit=50`) .expect(200); - dataRequest.then(response => { - const arr = handleIfRouteNotice(route, [ - { id: 1, date: '2017-01-01'}, - { id: 2, date: '2017-01-02'} - ]) - expect(response.body).to.deep.equal(arr); - done(); - }).catch(done); + dataRequest + .then((response) => { + const arr = handleIfRouteNotice(route, [ + { id: 1, date: "2017-01-01" }, + { id: 2, date: "2017-01-02" }, + ]); + expect(response.body).to.deep.equal(arr); + done(); + }) + .catch(done); }); - it('should respond with a 400 if db.query rejects', done => { - db.query = () => Promise.reject('This is a test of the emergency broadcast system.'); + it("should respond with a 400 if db.query rejects", (done) => { + db.query = () => + Promise.reject("This is a test of the emergency broadcast system."); const dataRequest = request(app) .get(`/${route}/agencies/fake-agency/reports/fake-report/data`) .expect(400); - dataRequest.then(response => { - expect(response.body).to.deep.equal({ - message: 'An error occurred. Please check the application logs.', - status: 400 - }); - done(); - }).catch(done); + dataRequest + .then((response) => { + expect(response.body).to.deep.equal({ + message: "An error occurred. Please check the application logs.", + status: 400, + }); + done(); + }) + .catch(done); }); - it('should respond with a 400 if the domain report is not one of the acceptable kinds of reports', done => { + it("should respond with a 400 if the domain report is not one of the acceptable kinds of reports", (done) => { db.query = (params) => { - expect(params.domain).to.equal('fakeiscool.gov'); - expect(params.reportName).to.equal('browser'); + expect(params.domain).to.equal("fakeiscool.gov"); + expect(params.reportName).to.equal("browser"); return Promise.resolve([ - { id: 1, date: new Date('2017-01-01'), data: { domain: 'fakeiscool.gov' } }, - { id: 2, date: new Date('2017-01-02'), data: { domain: 'bobtown.gov' } } + { + id: 1, + date: new Date("2017-01-01"), + data: { domain: "fakeiscool.gov" }, + }, + { + id: 2, + date: new Date("2017-01-02"), + data: { domain: "bobtown.gov" }, + }, ]); }; @@ -137,21 +154,30 @@ routes.forEach(route => { .get(`/${route}/domain/fakeiscool.gov/reports/browser/data`) .expect(400); - dataRequest.then(response => { - expect(response.body).to.deep.equal({ - message: 'You are requesting a report that cannot be filtered on domain. Please try one of the following reports: site, domain, download, second-level-domain.', - status: 400 - }); - done(); - }).catch(done); + dataRequest + .then((response) => { + expect(response.body).to.deep.equal({ + message: + "You are requesting a report that cannot be filtered on domain. Please try one of the following reports: site, domain, download, second-level-domain.", + status: 400, + }); + done(); + }) + .catch(done); }); - it('should pass params from the url to db.query and render the result for a domain query for a non-download report', done => { + it("should pass params from the url to db.query and render the result for a domain query for a non-download report", (done) => { db.query = (params) => { - expect(params.domain).to.equal('fakeiscool.gov'); - expect(params.reportName).to.equal('site'); - const arr = handleIfRouteNotice(route, [{ id: 1, date: new Date('2017-01-01'), report_name: 'site', data: { domain: 'fakeiscool.gov' } } - ]) + expect(params.domain).to.equal("fakeiscool.gov"); + expect(params.reportName).to.equal("site"); + const arr = handleIfRouteNotice(route, [ + { + id: 1, + date: new Date("2017-01-01"), + report_name: "site", + data: { domain: "fakeiscool.gov" }, + }, + ]); return Promise.resolve(arr); }; @@ -159,24 +185,46 @@ routes.forEach(route => { .get(`/${route}/domain/fakeiscool.gov/reports/site/data`) .expect(200); - dataRequest.then(response => { - const arr = handleIfRouteNotice(route, [ - { id: 1, date: '2017-01-01', report_name: 'site', domain: 'fakeiscool.gov' } - ]) - expect(response.body).to.deep.equal(arr); - done(); - }).catch(done); + dataRequest + .then((response) => { + const arr = handleIfRouteNotice(route, [ + { + id: 1, + date: "2017-01-01", + report_name: "site", + domain: "fakeiscool.gov", + }, + ]); + expect(response.body).to.deep.equal(arr); + done(); + }) + .catch(done); }); - it('should pass params from the url to db.query and render the result for a domain query for a download report', done => { + it("should pass params from the url to db.query and render the result for a domain query for a download report", (done) => { db.query = (params) => { - expect(params.domain).to.equal('fakeiscool.gov'); - expect(params.reportName).to.equal('download'); + expect(params.domain).to.equal("fakeiscool.gov"); + expect(params.reportName).to.equal("download"); const arr = handleIfRouteNotice(route, [ - { id: 1, date: new Date('2017-01-01'), report_name: 'download', data: { page: 'fakeiscool.gov/w8.pdf' } }, - { id: 2, date: new Date('2017-01-02'),report_name: 'download', data: { page: 'fakeiscool.gov/westworldtheshow/w8.pdf' } }, - { id: 3, date: new Date('2017-01-03'), report_name: 'download', data: { page: 'notiscool.gov/westworldtheshow/timewarpagain.pdf' } } - ]) + { + id: 1, + date: new Date("2017-01-01"), + report_name: "download", + data: { page: "fakeiscool.gov/w8.pdf" }, + }, + { + id: 2, + date: new Date("2017-01-02"), + report_name: "download", + data: { page: "fakeiscool.gov/westworldtheshow/w8.pdf" }, + }, + { + id: 3, + date: new Date("2017-01-03"), + report_name: "download", + data: { page: "notiscool.gov/westworldtheshow/timewarpagain.pdf" }, + }, + ]); return Promise.resolve(arr); }; @@ -184,47 +232,64 @@ routes.forEach(route => { .get(`/${route}/domain/fakeiscool.gov/reports/download/data`) .expect(200); - dataRequest.then(response => { - const arr = handleIfRouteNotice(route, [ - { id: 1, date: '2017-01-01', page: 'fakeiscool.gov/w8.pdf', report_name: 'download' }, - { id: 2, date: '2017-01-02', report_name: 'download', page: 'fakeiscool.gov/westworldtheshow/w8.pdf' } - ]) - expect(response.body).to.deep.equal(arr); - done(); - }).catch(done); + dataRequest + .then((response) => { + const arr = handleIfRouteNotice(route, [ + { + id: 1, + date: "2017-01-01", + page: "fakeiscool.gov/w8.pdf", + report_name: "download", + }, + { + id: 2, + date: "2017-01-02", + report_name: "download", + page: "fakeiscool.gov/westworldtheshow/w8.pdf", + }, + ]); + expect(response.body).to.deep.equal(arr); + done(); + }) + .catch(done); }); }); -}) +}); describe(`app with unspupported version`, () => { - beforeEach(() => { - db.query = () => Promise.resolve(); - }); - - it('should not accept unsupported versions', done => { - db.query = (params) => { - expect(params.reportAgency).to.equal('fake-agency'); - expect(params.reportName).to.equal('fake-report'); - const arr = handleIfRouteNotice(route, [ - { id: 1, date: new Date('2017-01-01')}, - { id: 2, date: new Date('2017-01-02')} - ]) - return Promise.resolve(arr) - }; - - const unspupportedVersion = 'v2.x' - const expectedErrorMessage = 'Version not found. Visit https://analytics.usa.gov/developer for information on the latest supported version.'; - - const dataRequest = request(app) - .get(`/${unspupportedVersion}/agencies/fake-agency/reports/fake-report/data`) - .expect(404); + beforeEach(() => { + db.query = () => Promise.resolve(); + }); - dataRequest.then(response => { + it("should not accept unsupported versions", (done) => { + db.query = (params) => { + expect(params.reportAgency).to.equal("fake-agency"); + expect(params.reportName).to.equal("fake-report"); + const arr = handleIfRouteNotice(route, [ + { id: 1, date: new Date("2017-01-01") }, + { id: 2, date: new Date("2017-01-02") }, + ]); + return Promise.resolve(arr); + }; + + const unspupportedVersion = "v2.x"; + const expectedErrorMessage = + "Version not found. Visit https://analytics.usa.gov/developer for information on the latest supported version."; + + const dataRequest = request(app) + .get( + `/${unspupportedVersion}/agencies/fake-agency/reports/fake-report/data`, + ) + .expect(404); + + dataRequest + .then((response) => { expect(response).to.include({ _body: expectedErrorMessage, - status: 404 + status: 404, }); done(); - }).catch(done); - }); + }) + .catch(done); + }); }); diff --git a/test/db.test.js b/test/db.test.js index e24e895..8485cfc 100644 --- a/test/db.test.js +++ b/test/db.test.js @@ -1,289 +1,578 @@ -const expect = require('chai').expect; -const proxyquire = require('proxyquire'); -const databaseSupport = require('./support/db'); +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const database = require("./support/db"); -const db = proxyquire('../src/db', { - './config': databaseSupport.config +const db = proxyquire("../src/db", { + "./config": require("../src/config"), }); -const routes =[ 'v1.1', 'v2' ] +describe("db", () => { + const apiVersions = ["v1.1", "v2"]; + before((done) => { + // Setup the test database client + database.createClient().then(() => done()); + }); -routes.forEach(route => { - describe(`db with ${route}`, () => { - - const table = route === 'v1.1' ? "analytics_data" : "analytics_data_ga4" - const queryVersion = route === `v1.1` ? '1.1' : '2' + after((done) => { + // Clean up the test database client and the application database client + database + .destroyClient() + .then(() => { + return db.dbClient.destroy(); + }) + .then(() => done()); + }); - beforeEach(done => { - databaseSupport.resetSchema(table).then(() => done()); - }); + apiVersions.forEach((apiVersion) => { + describe(`for API version ${apiVersion}`, () => { + const table = + apiVersion === "v1.1" ? "analytics_data" : "analytics_data_ga4"; + const queryVersion = apiVersion === `v1.1` ? "1.1" : "2"; - describe('.query(params)', () => { - it('should return all rows for the given agency and report', done => { - databaseSupport.client(table).insert([ - { report_name: 'my-report', report_agency: 'my-agency' }, - { report_name: 'not-my-report', report_agency: 'my-agency' }, - { report_name: 'my-report', report_agency: 'not-my-agency' }, - { report_name: 'my-report', report_agency: null } - ]).then(() => { - return db.query({ reportName: 'my-report', reportAgency: 'my-agency', version: queryVersion }); - }).then(results => { - expect(results).to.have.length(1); - expect(results[0].report_name).to.equal('my-report'); - expect(results[0].report_agency).to.equal('my-agency'); - done(); - }) - .catch(done); + beforeEach((done) => { + database.resetSchema(table).then(() => done()); }); - it('should return all rows without an agency if no agency name is given', done => { - databaseSupport.client(table).insert([ - { report_name: 'my-report', report_agency: 'not-my-agency' }, - { report_name: 'my-report', report_agency: null } - ]).then(() => { - return db.query({ reportName: 'my-report' , version: queryVersion }); - }).then(results => { - expect(results).to.have.length(1); - expect(results[0].report_name).to.equal('my-report'); - expect(results[0].report_agency).to.be.null; - done(); - }) - .catch(done); - }); + describe(".query(params)", () => { + it("should return all rows for the given agency and report", (done) => { + database + .client(table) + .insert([ + { report_name: "my-report", report_agency: "my-agency" }, + { report_name: "not-my-report", report_agency: "my-agency" }, + { report_name: "my-report", report_agency: "not-my-agency" }, + { report_name: "my-report", report_agency: null }, + ]) + .then(() => { + return db.query({ + reportName: "my-report", + reportAgency: "my-agency", + version: queryVersion, + }); + }) + .then((results) => { + expect(results).to.have.length(1); + expect(results[0].report_name).to.equal("my-report"); + expect(results[0].report_agency).to.equal("my-agency"); + done(); + }) + .catch(done); + }); - it('should sort the rows according to the date column', done => { - databaseSupport.client(table).insert([ - { report_name: 'report', date: '2017-01-02' }, - { report_name: 'report', date: '2017-01-01' }, - { report_name: 'report', date: '2017-01-03' } - ]).then(() => { - return db.query({ reportName: 'report' , version: queryVersion}); - }).then(results => { - expect(results).to.have.length(3); - results.forEach((result, index) => { - const resultDate = result.date.toISOString().slice(0, 10); - const expectedDate = `2017-01-0${3 - index}`; - expect(resultDate).to.equal(expectedDate); - }); - done(); - }) - .catch(done); - }); + it("should return all rows without an agency if no agency name is given", (done) => { + database + .client(table) + .insert([ + { report_name: "my-report", report_agency: "not-my-agency" }, + { report_name: "my-report", report_agency: null }, + ]) + .then(() => { + return db.query({ + reportName: "my-report", + version: queryVersion, + }); + }) + .then((results) => { + expect(results).to.have.length(1); + expect(results[0].report_name).to.equal("my-report"); + expect(results[0].report_agency).to.be.null; + done(); + }) + .catch(done); + }); - it('should limit the rows according to the limit param', done => { - const rows = Array(5).fill(0).map(() => { - return { report_name: 'report', date: '2017-01-01' }; + it("should sort the rows according to the date column", (done) => { + database + .client(table) + .insert([ + { report_name: "report", date: "2017-01-02" }, + { report_name: "report", date: "2017-01-01" }, + { report_name: "report", date: "2017-01-03" }, + ]) + .then(() => { + return db.query({ reportName: "report", version: queryVersion }); + }) + .then((results) => { + expect(results).to.have.length(3); + results.forEach((result, index) => { + const resultDate = result.date.toISOString().slice(0, 10); + const expectedDate = `2017-01-0${3 - index}`; + expect(resultDate).to.equal(expectedDate); + }); + done(); + }) + .catch(done); }); - databaseSupport.client(table).insert(rows).then(() => { - return db.query({ reportName: 'report', limit: 4 , version: queryVersion}); - }).then(results => { - expect(results).to.have.length(4); - done(); - }) - .catch(done); - }); - it('should default to a limit of 1000', done => { - const rows = Array(1001).fill(0).map(() => { - return { report_name: 'report', date: '2017-01-01' }; + it("should limit the rows according to the limit param", (done) => { + const rows = Array(5) + .fill(0) + .map(() => { + return { report_name: "report", date: "2017-01-01" }; + }); + database + .client(table) + .insert(rows) + .then(() => { + return db.query({ + reportName: "report", + limit: 4, + version: queryVersion, + }); + }) + .then((results) => { + expect(results).to.have.length(4); + done(); + }) + .catch(done); + }); + + it("should default to a limit of 1000", (done) => { + const rows = Array(1001) + .fill(0) + .map(() => { + return { report_name: "report", date: "2017-01-01" }; + }); + database + .client(table) + .insert(rows) + .then(() => { + return db.query({ reportName: "report", version: queryVersion }); + }) + .then((results) => { + expect(results).to.have.length(1000); + done(); + }) + .catch(done); }); - databaseSupport.client(table).insert(rows).then(() => { - return db.query({ reportName: 'report' , version: queryVersion}); - }).then(results => { - expect(results).to.have.length(1000); - done(); - }) - .catch(done); - }); - it('should have a maximum limit of 10,000', done => { - const rows = Array(11000).fill(0).map(() => { - return { report_name: 'report', date: '2017-01-01' }; + it("should have a maximum limit of 10,000", (done) => { + const rows = Array(11000) + .fill(0) + .map(() => { + return { report_name: "report", date: "2017-01-01" }; + }); + database + .client(table) + .insert(rows) + .then(() => { + return db.query({ + reportName: "report", + limit: 11000, + version: queryVersion, + }); + }) + .then((results) => { + expect(results).to.have.length(10000); + done(); + }) + .catch((err) => { + done(err); + }); }); - databaseSupport.client(table).insert(rows).then(() => { - return db.query({ reportName: 'report', limit: 11000 , version: queryVersion}); - }).then(results => { - expect(results).to.have.length(10000); - done(); - }) - .catch(err => { - done(err); + + it("should paginate on the page param", (done) => { + const rows = Array(6) + .fill(0) + .map((val, index) => { + return { report_name: "report", date: `2017-01-0${index + 1}` }; + }); + database + .client(table) + .insert(rows) + .then(() => { + return db.query({ + reportName: "report", + limit: 3, + page: 1, + version: queryVersion, + }); + }) + .then((results) => { + expect(results).to.have.length(3); + expect(results[0].date.toISOString()).to.match(/^2017-01-06/); + expect(results[2].date.toISOString()).to.match(/^2017-01-04/); + + return db.query({ + reportName: "report", + limit: 3, + page: 2, + version: queryVersion, + }); + }) + .then((results) => { + expect(results).to.have.length(3); + expect(results[0].date.toISOString()).to.match(/^2017-01-03/); + expect(results[2].date.toISOString()).to.match(/^2017-01-01/); + done(); + }) + .catch(done); }); }); - it('should paginate on the page param', done => { - const rows = Array(6).fill(0).map((val, index) => { - return { report_name: 'report', date: `2017-01-0${index + 1}` }; + describe(".buildTimeQuery(before, after)", () => { + it("should return an array containing true if no date params are present", () => { + const result = db.buildTimeQuery(null, null); + expect(result).to.deep.equal([true]); }); - databaseSupport.client(table).insert(rows).then(() => { - return db.query({ reportName: 'report', limit: 3, page: 1 , version: queryVersion }); - }).then(results => { - expect(results).to.have.length(3); - expect(results[0].date.toISOString()).to.match(/^2017-01-06/); - expect(results[2].date.toISOString()).to.match(/^2017-01-04/); - return db.query({ reportName: 'report', limit: 3, page: 2 , version: queryVersion}); - }) - .then(results => { - expect(results).to.have.length(3); - expect(results[0].date.toISOString()).to.match(/^2017-01-03/); - expect(results[2].date.toISOString()).to.match(/^2017-01-01/); - done(); - }) - .catch(done); - }); - }); + it("should return a nested array a raw query string and an array of the dates if both a params are set", () => { + const result = db.buildTimeQuery("2018-11-20", "2018-12-20"); + expect(result).to.deep.equal([ + '"date" <= ?::date AND "date" >= ?::date', + ["2018-11-20", "2018-12-20"], + ]); + }); - describe('.buildTimeQuery(before, after)', ()=> { - it('should return an array containing true if no date params are present', ()=>{ - const result = db.buildTimeQuery(null, null); - expect(result).to.deep.equal([true]); - }); - it('should return a nested array a raw query string and an array of the dates if both a params are set', () => { - const result = db.buildTimeQuery('2018-11-20', '2018-12-20'); - expect(result).to.deep.equal(['"date" <= ?::date AND "date" >= ?::date', ['2018-11-20', '2018-12-20']]); - }); - it('should return a nested array a raw query string and an array of the before if before is set', () => { - const result = db.buildTimeQuery('2018-11-20', null); - expect(result).to.deep.equal(['"date" <= ?::date', ['2018-11-20']]); - }); - it('should return a nested array a raw query string and an array of the after if after is set', () => { - const result = db.buildTimeQuery(null, '2018-11-22'); - expect(result).to.deep.equal(['"date" >= ?::date', ['2018-11-22']]); - }); - }); - describe('.queryDomain(params)', ()=>{ - it('should only return 2 results that include site reports from the test.gov domain', done => { - databaseSupport.client(table).insert( - [{ report_name: 'site', date: '2017-01-02', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2017-01-01', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2017-01-03', data: { domain: 'test.gov' } }] - ).then(() => { - return db.queryDomain('test.gov', 'site', 2, 1, null, null, table); - }).then(results => { - expect(results).to.have.length(2); - done(); - }) - .catch(err => { - done(err); - }); - }); - it('should only return 2 results that include site reports from the test.gov domain, when multiple reports', done => { - databaseSupport.client(table).insert( - [{ report_name: 'report', date: '2017-01-02', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2017-01-01', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2017-01-03', data: { domain: 'test.gov' } }] - ).then(() => { - return db.queryDomain('test.gov', 'site', 1000, 1, null, null, table); - }).then(results => { - expect(results).to.have.length(2); - expect(results[0].report_name).to.equal('site'); - expect(results[0].data.domain).to.equal('test.gov'); - done(); - }) - .catch(err => { - done(err); - }); - }); - it('should only return 2 results that include site reports from the test.gov domain, when multiple domains', done => { - databaseSupport.client(table).insert( - [{ report_name: 'site', date: '2017-01-02', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2017-01-01', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2017-01-03', data: { domain: 'usda.gov' } }] - ).then(() => { - return db.queryDomain('test.gov', 'site', 1000, 1, null, null, table); - }).then(results => { - expect(results).to.have.length(2); - expect(results[0].report_name).to.equal('site'); - expect(results[0].data.domain).to.equal('test.gov'); - done(); - }) - .catch(err => { - done(err); - }); - }); + it("should return a nested array a raw query string and an array of the before if before is set", () => { + const result = db.buildTimeQuery("2018-11-20", null); + expect(result).to.deep.equal(['"date" <= ?::date', ["2018-11-20"]]); + }); - it('should only return 2 results that include site reports from the test.gov domain, when before date parameters are in', done => { - databaseSupport.client(table).insert( - [{ report_name: 'site', date: '2017-01-02', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2017-01-01', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2018-01-03', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2018-01-03', data: { domain: 'usda.gov' } }] - ).then(() => { - return db.queryDomain('test.gov', 'site', 1000, 1, '2017-10-20', null, table); - }).then(results => { - expect(results).to.have.length(2); - expect(results[0].report_name).to.equal('site'); - expect(results[0].data.domain).to.equal('test.gov'); - expect(results[0].date.toISOString()).to.match(/^2017-01-02/); - done(); - }) - .catch(err => { - done(err); - }); - }); - it('should only return 1 result that include site reports from the test.gov domain, when after date parameters are in', done => { - databaseSupport.client(table).insert( - [{ report_name: 'site', date: '2017-01-02', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2017-01-01', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2018-01-03', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2018-01-03', data: { domain: 'usda.gov' } }] - ).then(() => { - return db.queryDomain('test.gov', 'site', 1000, 1, null, '2017-10-20', table); - }).then(results => { - expect(results).to.have.length(1); - expect(results[0].report_name).to.equal('site'); - expect(results[0].data.domain).to.equal('test.gov'); - expect(results[0].date.toISOString()).to.match(/^2018-01-03/); - done(); - }) - .catch(err => { - done(err); - }); - }); - it('should only return 2 result that include site reports from the test.gov domain, when after/before date parameters set', done => { - databaseSupport.client(table).insert( - [{ report_name: 'site', date: '2017-01-02', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2017-01-01', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2018-01-03', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2017-11-04', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2017-11-03', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2018-01-03', data: { domain: 'usda.gov' } }] - ).then(() => { - return db.queryDomain('test.gov', 'site', 1000, 1, '2018-01-02', '2017-10-20', table); - }).then(results => { - expect(results).to.have.length(2); - expect(results[0].report_name).to.equal('site'); - expect(results[0].data.domain).to.equal('test.gov'); - expect(results[0].date.toISOString()).to.match(/^2017-11-04/); - done(); - }) - .catch(err => { - done(err); - }); + it("should return a nested array a raw query string and an array of the after if after is set", () => { + const result = db.buildTimeQuery(null, "2018-11-22"); + expect(result).to.deep.equal(['"date" >= ?::date', ["2018-11-22"]]); + }); }); - it('should only return 2 result that include site reports from the test.gov domain, when after/before date parameters set', done => { - databaseSupport.client(table).insert( - [{ report_name: 'site', date: '2017-01-02', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2017-01-01', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2018-01-03', data: { domain: 'test.gov' } }, - { report_name: 'report', date: '2018-01-03', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2017-11-03', data: { domain: 'test.gov' } }, - { report_name: 'site', date: '2018-01-03', data: { domain: 'usda.gov' } }] - ).then(() => { - return db.queryDomain('test.gov', 'site', 1000, 1, '2018-01-04', '2017-10-20', table); - }).then(results => { - expect(results).to.have.length(2); - expect(results[0].report_name).to.equal('site'); - expect(results[0].data.domain).to.equal('test.gov'); - expect(results[0].date.toISOString()).to.match(/^2018-01-03/); - done(); - }) - .catch(err => { - done(err); - }); + + describe(".queryDomain(params)", () => { + it("should only return 2 results that include site reports from the test.gov domain", (done) => { + database + .client(table) + .insert([ + { + report_name: "site", + date: "2017-01-02", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2017-01-01", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2017-01-03", + data: { domain: "test.gov" }, + }, + ]) + .then(() => { + return db.queryDomain( + "test.gov", + "site", + 2, + 1, + null, + null, + table, + ); + }) + .then((results) => { + expect(results).to.have.length(2); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it("should only return 2 results that include site reports from the test.gov domain, when multiple reports", (done) => { + database + .client(table) + .insert([ + { + report_name: "report", + date: "2017-01-02", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2017-01-01", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2017-01-03", + data: { domain: "test.gov" }, + }, + ]) + .then(() => { + return db.queryDomain( + "test.gov", + "site", + 1000, + 1, + null, + null, + table, + ); + }) + .then((results) => { + expect(results).to.have.length(2); + expect(results[0].report_name).to.equal("site"); + expect(results[0].data.domain).to.equal("test.gov"); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it("should only return 2 results that include site reports from the test.gov domain, when multiple domains", (done) => { + database + .client(table) + .insert([ + { + report_name: "site", + date: "2017-01-02", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2017-01-01", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2017-01-03", + data: { domain: "usda.gov" }, + }, + ]) + .then(() => { + return db.queryDomain( + "test.gov", + "site", + 1000, + 1, + null, + null, + table, + ); + }) + .then((results) => { + expect(results).to.have.length(2); + expect(results[0].report_name).to.equal("site"); + expect(results[0].data.domain).to.equal("test.gov"); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it("should only return 2 results that include site reports from the test.gov domain, when before date parameters are in", (done) => { + database + .client(table) + .insert([ + { + report_name: "site", + date: "2017-01-02", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2017-01-01", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2018-01-03", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2018-01-03", + data: { domain: "usda.gov" }, + }, + ]) + .then(() => { + return db.queryDomain( + "test.gov", + "site", + 1000, + 1, + "2017-10-20", + null, + table, + ); + }) + .then((results) => { + expect(results).to.have.length(2); + expect(results[0].report_name).to.equal("site"); + expect(results[0].data.domain).to.equal("test.gov"); + expect(results[0].date.toISOString()).to.match(/^2017-01-02/); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it("should only return 1 result that include site reports from the test.gov domain, when after date parameters are in", (done) => { + database + .client(table) + .insert([ + { + report_name: "site", + date: "2017-01-02", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2017-01-01", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2018-01-03", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2018-01-03", + data: { domain: "usda.gov" }, + }, + ]) + .then(() => { + return db.queryDomain( + "test.gov", + "site", + 1000, + 1, + null, + "2017-10-20", + table, + ); + }) + .then((results) => { + expect(results).to.have.length(1); + expect(results[0].report_name).to.equal("site"); + expect(results[0].data.domain).to.equal("test.gov"); + expect(results[0].date.toISOString()).to.match(/^2018-01-03/); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it("should only return 2 result that include site reports from the test.gov domain, when after/before date parameters set", (done) => { + database + .client(table) + .insert([ + { + report_name: "site", + date: "2017-01-02", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2017-01-01", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2018-01-03", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2017-11-04", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2017-11-03", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2018-01-03", + data: { domain: "usda.gov" }, + }, + ]) + .then(() => { + return db.queryDomain( + "test.gov", + "site", + 1000, + 1, + "2018-01-02", + "2017-10-20", + table, + ); + }) + .then((results) => { + expect(results).to.have.length(2); + expect(results[0].report_name).to.equal("site"); + expect(results[0].data.domain).to.equal("test.gov"); + expect(results[0].date.toISOString()).to.match(/^2017-11-04/); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it("should only return 2 result that include site reports from the test.gov domain, when after/before date parameters set", (done) => { + database + .client(table) + .insert([ + { + report_name: "site", + date: "2017-01-02", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2017-01-01", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2018-01-03", + data: { domain: "test.gov" }, + }, + { + report_name: "report", + date: "2018-01-03", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2017-11-03", + data: { domain: "test.gov" }, + }, + { + report_name: "site", + date: "2018-01-03", + data: { domain: "usda.gov" }, + }, + ]) + .then(() => { + return db.queryDomain( + "test.gov", + "site", + 1000, + 1, + "2018-01-04", + "2017-10-20", + table, + ); + }) + .then((results) => { + expect(results).to.have.length(2); + expect(results[0].report_name).to.equal("site"); + expect(results[0].data.domain).to.equal("test.gov"); + expect(results[0].date.toISOString()).to.match(/^2018-01-03/); + done(); + }) + .catch((err) => { + done(err); + }); + }); }); }); }); -}) +}); diff --git a/test/support/db.js b/test/support/db.js index 9e947ed..13ad18d 100644 --- a/test/support/db.js +++ b/test/support/db.js @@ -1,10 +1,31 @@ -const knex = require('knex'); -const config = require('../../src/config'); +const knex = require("knex"); +const config = require("../../src/config"); -const client = knex({ client: 'pg', connection: config.postgres }); +class Database { + get client() { + return this.dbClient; + } -const resetSchema = (table) => { - return client(table).delete(); -}; + async createClient() { + if (this.dbClient) { + return; + } -module.exports = { client, config, resetSchema }; + this.dbClient = await knex({ client: "pg", connection: config.postgres }); + } + + async destroyClient() { + if (this.dbClient) { + await this.dbClient.destroy(); + this.dbClient = null; + } + + return; + } + + resetSchema(table) { + return this.dbClient(table).delete(); + } +} + +module.exports = new Database();