From 7846c499dcc57138b09f933b10bf730defca66a1 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Fri, 16 Feb 2024 10:47:17 -0500 Subject: [PATCH 01/11] Add API v1 -> v2 migration docs to README --- README.md | 82 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8593b03..d3bb810 100644 --- a/README.md +++ b/README.md @@ -50,41 +50,105 @@ is configured to write to the same database and run with the `--write-to-databas # 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/ + +## 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 ``` +## 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 + +##### New fields + +###### 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' + # Running the Tests The Analytics API application is backed by a test suite that uses From 752e0f7ebfb17864f508b0d29a1be2b35ab95164 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Fri, 16 Feb 2024 12:03:16 -0500 Subject: [PATCH 02/11] Add eslint/prettier, run lint:fix on repo --- eslint.config.js | 3 + index.js | 10 +- knexfile.js | 30 +- .../20170308164751_create_analytics_data.js | 22 +- ...170316115145_add_analytics_data_indexes.js | 26 +- ...20170522094056_rename_date_time_to_date.js | 25 +- ...10706213753_add_date_id_multi_col_index.js | 21 +- ...0231218165411_create_analytics_data_ga4.js | 24 +- ...0130203237_rename_date_time_to_date_ga4.js | 12 +- .../20240130203849_remove_version_col_ga4.js | 16 +- newrelic.js | 4 +- package-lock.json | 1750 ++--------------- package.json | 11 +- src/api-data-gov-filter.js | 12 +- src/app.js | 153 +- src/config.js | 6 +- src/db.js | 125 +- src/logger.js | 10 +- test/.eslintrc.yml | 6 - test/api-data-gov-filter.test.js | 54 +- test/app.test.js | 331 ++-- test/db.test.js | 711 ++++--- test/support/db.js | 6 +- 23 files changed, 1129 insertions(+), 2239 deletions(-) create mode 100644 eslint.config.js delete mode 100644 test/.eslintrc.yml 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..2090cb5 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,20 @@ 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' + user: process.env.CIRCLECI ? "postgres" : undefined, + database: "analytics-api-test", }, migrations: { - tableName: 'knex_migrations' - } - } + tableName: "knex_migrations", + }, + }, }; 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..2cedb16 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "pretest": "NODE_ENV=test npm run migrate", "test": "NODE_ENV=test `npm bin`/mocha test/**/*.test.js --exit", "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..894b512 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 }; 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..655785d 100644 --- a/test/db.test.js +++ b/test/db.test.js @@ -1,289 +1,540 @@ -const expect = require('chai').expect; -const proxyquire = require('proxyquire'); -const databaseSupport = require('./support/db'); +const expect = require("chai").expect; +const proxyquire = require("proxyquire"); +const databaseSupport = require("./support/db"); -const db = proxyquire('../src/db', { - './config': databaseSupport.config +const db = proxyquire("../src/db", { + "./config": databaseSupport.config, }); -const routes =[ 'v1.1', 'v2' ] +const routes = ["v1.1", "v2"]; - -routes.forEach(route => { +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"; - const table = route === 'v1.1' ? "analytics_data" : "analytics_data_ga4" - const queryVersion = route === `v1.1` ? '1.1' : '2' - - beforeEach(done => { + beforeEach((done) => { databaseSupport.resetSchema(table).then(() => done()); }); - 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); + 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); }); - 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); + 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); }); - 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 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 limit the rows according to the limit param', done => { - const rows = Array(5).fill(0).map(() => { - return { report_name: 'report', date: '2017-01-01' }; - }); - 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 limit the rows according to the limit param", (done) => { + const rows = Array(5) + .fill(0) + .map(() => { + return { report_name: "report", date: "2017-01-01" }; + }); + 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' }; - }); - 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 default to a limit of 1000", (done) => { + const rows = Array(1001) + .fill(0) + .map(() => { + return { report_name: "report", date: "2017-01-01" }; + }); + 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' }; - }); - 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 have a maximum limit of 10,000", (done) => { + const rows = Array(11000) + .fill(0) + .map(() => { + return { report_name: "report", date: "2017-01-01" }; + }); + 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}` }; - }); - 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/); + 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}` }; + }); + 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); + 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); }); }); - describe('.buildTimeQuery(before, after)', ()=> { - it('should return an array containing true if no date params are present', ()=>{ + 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 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 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']]); + 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 => { + 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 => { + .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 => { + 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 => { + 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 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 => { + 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 => { + 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 => { + 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 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 => { + 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); }); }); }); }); -}) +}); diff --git a/test/support/db.js b/test/support/db.js index 9e947ed..fa86c4a 100644 --- a/test/support/db.js +++ b/test/support/db.js @@ -1,7 +1,7 @@ -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 }); +const client = knex({ client: "pg", connection: config.postgres }); const resetSchema = (table) => { return client(table).delete(); From 0eebbaa7252c4f10ec1dd1359830f897295e3434 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Fri, 16 Feb 2024 12:43:36 -0500 Subject: [PATCH 03/11] Refactor CI, add lint stage --- .circleci/config.yml | 248 ++++++++++++++++++------------------------- 1 file changed, 101 insertions(+), 147 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 127acd6..6a2dfd6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,8 +1,53 @@ version: 2.1 + +restore_npm_cache: &restore_npm_cache + restore_cache: + keys: + - v1-dependencies-{{ checksum "package.json" }} + - v1-dependencies- + +install_npm_packages: &install_npm_packages + run: + name: install dependencies + command: npm install + +save_npm_cache: &save_npm_cache + save_cache: + paths: + - ./node_modules + key: v1-dependencies-{{ checksum "package.json" }} + +fix_file_suffixes_for_cloud_gov: &fix_file_suffixes_for_cloud_gov + 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 + +install_cf_cli: &install_cf_cli + 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 + jobs: - develop_deploy: + lint_js: docker: - - image: cimg/node:16.19.1-browsers + - image: cimg/node:20.11.0-browsers + steps: + - checkout + - *restore_npm_cache + - *install_npm_packages + - *save_npm_cache + - run: + name: lint javascript + command: npm run lint + test: + docker: + - image: cimg/node:20.11.0-browsers environment: POSTGRES_USER: postgres NODE_ENV: test @@ -11,25 +56,20 @@ jobs: 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" }} - + - *restore_npm_cache + - *install_npm_packages + - *save_npm_cache - run: - name: run tests - command: npm test - + name: unit test javascript + command: npm test + development_env_deploy: + docker: + - image: cimg/node:20.11.0-browsers + steps: + - checkout + - *restore_npm_cache + - *install_npm_packages + - *save_npm_cache - run: name: Replace Data URL in manifest.yml file when deploying to develop command: | @@ -39,26 +79,12 @@ jobs: 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 - + - *fix_file_suffixes_for_cloud_gov + - *install_cf_cli - run: name: deploy command: | @@ -71,37 +97,14 @@ jobs: cf set-env analytics-reporter-api-develop API_DATA_GOV_SECRET "$API_SECRET_LOWER" cf restage analytics-reporter-api-develop cf logout - - staging_deploy: + staging_env_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 + - image: cimg/node:20.11.0-browsers 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 - + - *restore_npm_cache + - *install_npm_packages + - *save_npm_cache - run: name: Replace Data URL in manifest.yml file when deploying to staging command: | @@ -111,26 +114,12 @@ jobs: 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 - + - *fix_file_suffixes_for_cloud_gov + - *install_cf_cli - run: name: deploy command: | @@ -143,37 +132,14 @@ jobs: cf set-env analytics-reporter-api-staging API_DATA_GOV_SECRET "$API_SECRET_LOWER" cf restage analytics-reporter-api-staging cf logout - - main_deploy: + production_env_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 + - image: cimg/node:20.11.0-browsers 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 - + - *restore_npm_cache + - *install_npm_packages + - *save_npm_cache - run: name: Replace Data URL in manifest.yml file when deploying to production command: | @@ -183,26 +149,12 @@ jobs: 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 - + - *fix_file_suffixes_for_cloud_gov + - *install_cf_cli - run: name: deploy command: | @@ -216,28 +168,30 @@ jobs: cf restage analytics-reporter-api-production cf logout -workflows: - develop_workflow: - jobs: - - develop_deploy: - filters: - branches: - only: +ci: + jobs: + - lint + - test: + requires: + - lint + - development_env_deploy: + requires: + - test + filters: + branches: + only: - develop - - staging: - jobs: - - staging_deploy: - filters: - branches: - only: - - stage - - main_workflow: - jobs: - - main_deploy: - filters: - branches: - only: + - staging_env_deploy: + requires: + - test + filters: + branches: + only: + - staging + - production_env_deploy: + requires: + - test + filters: + branches: + only: - master - From b82d53aa2d2227547f05ff966fc54c4ac281355c Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Fri, 16 Feb 2024 13:47:56 -0500 Subject: [PATCH 04/11] Add github actions for lint and test --- .github/workflows/ci.yml | 80 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9072a80 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +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: Create Checksum of package.json + run: echo PACKAGE_CHECKSUM="$(shasum package.json | awk '{ print $1 }')" >> "$GITHUB_ENV" + - name: Restore Cache + uses: actions/cache/restore@v4 + with: + path: ./node_modules + key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} + - name: Install Dependencies + run: npm install + - name: Save Cache + uses: actions/cache/save@v4 + id: cache + with: + path: ./node_modules + key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} + - 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: postgres + POSTGRES_PASSWORD: postgres + 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: Create Checksum of package.json + run: echo PACKAGE_CHECKSUM="$(shasum package.json | awk '{ print $1 }')" >> "$GITHUB_ENV" + - name: Restore Cache + uses: actions/cache/restore@v4 + with: + path: ./node_modules + key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} + - name: Install Dependencies + run: npm install + - name: Save Cache + uses: actions/cache/save@v4 + id: cache + with: + path: ./node_modules + key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} + - name: Run tests + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: analytics_reporter_test + POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} + run: npm test From e55043eb7f7e0bd9dbb610561d7725b814e666e7 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Fri, 16 Feb 2024 14:01:21 -0500 Subject: [PATCH 05/11] Update local testing setup Add docker-compose.test.yml for a local test DB Add mocharc with default files to test so they can be omitted from the test command Update the knexfile to provide some defaults for dev/test envs Add lint and test to the README Disable lint/test in circleCI --- .circleci/config.yml | 28 - .github/workflows/ci.yml | 13 +- .mocharc.yml | 11 + README.md | 34 +- docker-compose.test.yml | 10 + knexfile.js | 6 +- package.json | 2 +- src/db.js | 2 +- test/db.test.js | 1058 ++++++++++++++++++++------------------ test/support/db.js | 31 +- 10 files changed, 632 insertions(+), 563 deletions(-) create mode 100644 .mocharc.yml create mode 100644 docker-compose.test.yml diff --git a/.circleci/config.yml b/.circleci/config.yml index 6a2dfd6..85af679 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -34,34 +34,6 @@ install_cf_cli: &install_cf_cli sudo dpkg -i cf8-cli-installer_8.7.4_x86-64.deb jobs: - lint_js: - docker: - - image: cimg/node:20.11.0-browsers - steps: - - checkout - - *restore_npm_cache - - *install_npm_packages - - *save_npm_cache - - run: - name: lint javascript - command: npm run lint - test: - docker: - - image: cimg/node:20.11.0-browsers - environment: - POSTGRES_USER: postgres - NODE_ENV: test - - image: circleci/postgres:9.5-alpine - environment: - POSTGRES_DB: analytics-api-test - steps: - - checkout - - *restore_npm_cache - - *install_npm_packages - - *save_npm_cache - - run: - name: unit test javascript - command: npm test development_env_deploy: docker: - image: cimg/node:20.11.0-browsers diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9072a80..d505012 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: with: path: ./node_modules key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} - - name: Lint JavaScript + - name: lint javascript run: npm run lint test: needs: lint @@ -39,8 +39,8 @@ jobs: image: postgres:latest env: POSTGRES_DB: analytics_reporter_test - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres + POSTGRES_USER: analytics + POSTGRES_PASSWORD: 123abc ports: - 5432:5432 options: @@ -71,10 +71,5 @@ jobs: with: path: ./node_modules key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} - - name: Run tests - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: analytics_reporter_test - POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} + - name: run tests run: npm test 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 d3bb810..633b087 100644 --- a/README.md +++ b/README.md @@ -149,21 +149,41 @@ An enum which describes the session. Possible values: 'Direct', 'Organic Search', 'Paid Social', 'Organic Social', 'Email', 'Affiliates', 'Referral', 'Paid Search', 'Video', and 'Display' -# Running the Tests +# Linting -The Analytics API application is backed by a test suite that uses -[Mocha](https://mochajs.org/) to run tests. +This repo uses Eslint and Prettier for code static analysis and formatting. Run +the linter with: -Before running the test suite, a database for the tests will need to be created. +```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 ``` 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/knexfile.js b/knexfile.js index 2090cb5..79bc1f1 100644 --- a/knexfile.js +++ b/knexfile.js @@ -26,8 +26,10 @@ module.exports = { test: { 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", diff --git a/package.json b/package.json index 2cedb16..9cc6c7e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "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": "eslint .", "lint:fix": "eslint . --fix" diff --git a/src/db.js b/src/db.js index 894b512..44be3c5 100644 --- a/src/db.js +++ b/src/db.js @@ -125,4 +125,4 @@ const query = ({ ); }; -module.exports = { query, queryDomain, buildTimeQuery }; +module.exports = { query, queryDomain, buildTimeQuery, dbClient: db }; diff --git a/test/db.test.js b/test/db.test.js index 655785d..8485cfc 100644 --- a/test/db.test.js +++ b/test/db.test.js @@ -1,539 +1,577 @@ const expect = require("chai").expect; const proxyquire = require("proxyquire"); -const databaseSupport = require("./support/db"); +const database = require("./support/db"); const db = proxyquire("../src/db", { - "./config": databaseSupport.config, + "./config": require("../src/config"), }); -const routes = ["v1.1", "v2"]; +describe("db", () => { + const apiVersions = ["v1.1", "v2"]; -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"; + before((done) => { + // Setup the test database client + database.createClient().then(() => done()); + }); - beforeEach((done) => { - databaseSupport.resetSchema(table).then(() => done()); - }); + after((done) => { + // Clean up the test database client and the application database client + database + .destroyClient() + .then(() => { + return db.dbClient.destroy(); + }) + .then(() => done()); + }); - 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); - }); + 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"; - 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); + beforeEach((done) => { + database.resetSchema(table).then(() => 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); + 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 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 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); + }); + + 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" }; }); - done(); - }) - .catch(done); - }); + 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 limit the rows according to the limit param", (done) => { - const rows = Array(5) - .fill(0) - .map(() => { - return { report_name: "report", date: "2017-01-01" }; - }); - databaseSupport - .client(table) - .insert(rows) - .then(() => { - return db.query({ - reportName: "report", - limit: 4, - version: queryVersion, + it("should default to a limit of 1000", (done) => { + const rows = Array(1001) + .fill(0) + .map(() => { + return { report_name: "report", date: "2017-01-01" }; }); - }) - .then((results) => { - expect(results).to.have.length(4); - done(); - }) - .catch(done); + database + .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" }; + }); + 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); + }); + }); + + 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 default to a limit of 1000", (done) => { - const rows = Array(1001) - .fill(0) - .map(() => { - return { report_name: "report", date: "2017-01-01" }; - }); - databaseSupport - .client(table) - .insert(rows) - .then(() => { - return db.query({ reportName: "report", version: queryVersion }); - }) - .then((results) => { - expect(results).to.have.length(1000); - done(); - }) - .catch(done); + 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"]]); + }); }); - 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" }; - }); - databaseSupport - .client(table) - .insert(rows) - .then(() => { - return db.query({ - reportName: "report", - limit: 11000, - version: queryVersion, + 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); }); - }) - .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}` }; - }); - databaseSupport - .client(table) - .insert(rows) - .then(() => { - return db.query({ - reportName: "report", - limit: 3, - page: 1, - version: queryVersion, + 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); }); - }) - .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, + 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); }); - }) - .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); - }); - }); + }); - 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 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 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 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); - }); + 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 fa86c4a..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 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(); From da0e27b8bb2186cb49bda2810bb1a73232f01249 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Tue, 20 Feb 2024 17:03:30 -0500 Subject: [PATCH 06/11] Replace CircleCI deployments with Github Actions --- .circleci/config.yml | 169 ----------------------------------- .github/workflows/ci.yml | 75 +++++++++++++--- .github/workflows/deploy.yml | 79 ++++++++++++++++ entrypoint.sh | 1 - manifest.yml | 8 +- 5 files changed, 146 insertions(+), 186 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/deploy.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 85af679..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,169 +0,0 @@ -version: 2.1 - -restore_npm_cache: &restore_npm_cache - restore_cache: - keys: - - v1-dependencies-{{ checksum "package.json" }} - - v1-dependencies- - -install_npm_packages: &install_npm_packages - run: - name: install dependencies - command: npm install - -save_npm_cache: &save_npm_cache - save_cache: - paths: - - ./node_modules - key: v1-dependencies-{{ checksum "package.json" }} - -fix_file_suffixes_for_cloud_gov: &fix_file_suffixes_for_cloud_gov - 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 - -install_cf_cli: &install_cf_cli - 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 - -jobs: - development_env_deploy: - docker: - - image: cimg/node:20.11.0-browsers - steps: - - checkout - - *restore_npm_cache - - *install_npm_packages - - *save_npm_cache - - 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 - - *fix_file_suffixes_for_cloud_gov - - *install_cf_cli - - 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_env_deploy: - docker: - - image: cimg/node:20.11.0-browsers - steps: - - checkout - - *restore_npm_cache - - *install_npm_packages - - *save_npm_cache - - 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 - - *fix_file_suffixes_for_cloud_gov - - *install_cf_cli - - 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 - production_env_deploy: - docker: - - image: cimg/node:20.11.0-browsers - steps: - - checkout - - *restore_npm_cache - - *install_npm_packages - - *save_npm_cache - - 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 - - *fix_file_suffixes_for_cloud_gov - - *install_cf_cli - - 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 - -ci: - jobs: - - lint - - test: - requires: - - lint - - development_env_deploy: - requires: - - test - filters: - branches: - only: - - develop - - staging_env_deploy: - requires: - - test - filters: - branches: - only: - - staging - - production_env_deploy: - requires: - - test - filters: - branches: - only: - - master diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d505012..b0e85b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,23 +11,23 @@ jobs: - name: Install Node uses: actions/setup-node@v4 with: - node-version: "lts/*" - cache: 'npm' + node-version: "lts/*" + cache: 'npm' - name: Create Checksum of package.json run: echo PACKAGE_CHECKSUM="$(shasum package.json | awk '{ print $1 }')" >> "$GITHUB_ENV" - name: Restore Cache uses: actions/cache/restore@v4 with: - path: ./node_modules - key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} + path: ./node_modules + key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} - name: Install Dependencies run: npm install - name: Save Cache uses: actions/cache/save@v4 id: cache with: - path: ./node_modules - key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} + path: ./node_modules + key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} - name: lint javascript run: npm run lint test: @@ -54,22 +54,73 @@ jobs: - name: Install Node uses: actions/setup-node@v4 with: - node-version: "lts/*" - cache: 'npm' + node-version: "lts/*" + cache: 'npm' - name: Create Checksum of package.json run: echo PACKAGE_CHECKSUM="$(shasum package.json | awk '{ print $1 }')" >> "$GITHUB_ENV" - name: Restore Cache uses: actions/cache/restore@v4 with: - path: ./node_modules - key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} + path: ./node_modules + key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} - name: Install Dependencies run: npm install - name: Save Cache uses: actions/cache/save@v4 id: cache with: - path: ./node_modules - key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} + path: ./node_modules + key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} - 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..44eabfc --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,79 @@ +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: + +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: Create Checksum of package.json + run: echo PACKAGE_CHECKSUM="$(shasum package.json | awk '{ print $1 }')" >> "$GITHUB_ENV" + - name: Restore Cache + uses: actions/cache/restore@v4 + with: + path: ./node_modules + key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} + - name: Install Dependencies + run: npm install + - name: Save Cache + uses: actions/cache/save@v4 + id: cache + with: + path: ./node_modules + key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} + - 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" + cf set-env analytics-reporter-api-develop API_DATA_GOV_SECRET "$API_DATA_GOV_SECRET" + cf restage analytics-reporter-api-develop + cf logout 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/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 From ef64da49c5db2c881d3e8d7b8177312c6d35f3e1 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Wed, 21 Feb 2024 09:19:04 -0500 Subject: [PATCH 07/11] Use env vars correctly in deploy workflow --- .github/workflows/deploy.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 44eabfc..69635cf 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,6 +24,17 @@ on: 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 @@ -74,6 +85,6 @@ jobs: cf login -u $CF_USERNAME -p $CF_PASSWORD -o $ORGANIZATION_NAME -s $SPACE_NAME cat manifest.yml cf push -f "./manifest.yml" - cf set-env analytics-reporter-api-develop API_DATA_GOV_SECRET "$API_DATA_GOV_SECRET" - cf restage analytics-reporter-api-develop + cf set-env $APP_NAME API_DATA_GOV_SECRET "$API_DATA_GOV_SECRET" + cf restage $APP_NAME cf logout From f102faf53e704dd5154120bb3d21f0e584133919 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Wed, 21 Feb 2024 09:36:22 -0500 Subject: [PATCH 08/11] Remove duplicated caching logic from workflows --- .github/workflows/ci.yml | 34 ++++------------------------------ .github/workflows/deploy.yml | 17 ++--------------- 2 files changed, 6 insertions(+), 45 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0e85b7..b270d20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,21 +13,8 @@ jobs: with: node-version: "lts/*" cache: 'npm' - - name: Create Checksum of package.json - run: echo PACKAGE_CHECKSUM="$(shasum package.json | awk '{ print $1 }')" >> "$GITHUB_ENV" - - name: Restore Cache - uses: actions/cache/restore@v4 - with: - path: ./node_modules - key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} - - name: Install Dependencies - run: npm install - - name: Save Cache - uses: actions/cache/save@v4 - id: cache - with: - path: ./node_modules - key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} + - name: install dependencies + run: npm ci - name: lint javascript run: npm run lint test: @@ -56,21 +43,8 @@ jobs: with: node-version: "lts/*" cache: 'npm' - - name: Create Checksum of package.json - run: echo PACKAGE_CHECKSUM="$(shasum package.json | awk '{ print $1 }')" >> "$GITHUB_ENV" - - name: Restore Cache - uses: actions/cache/restore@v4 - with: - path: ./node_modules - key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} - - name: Install Dependencies - run: npm install - - name: Save Cache - uses: actions/cache/save@v4 - id: cache - with: - path: ./node_modules - key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} + - name: install dependencies + run: npm ci - name: run tests run: npm test deploy_dev: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 69635cf..889de9e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -46,21 +46,8 @@ jobs: with: node-version: "lts/*" cache: 'npm' - - name: Create Checksum of package.json - run: echo PACKAGE_CHECKSUM="$(shasum package.json | awk '{ print $1 }')" >> "$GITHUB_ENV" - - name: Restore Cache - uses: actions/cache/restore@v4 - with: - path: ./node_modules - key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} - - name: Install Dependencies - run: npm install - - name: Save Cache - uses: actions/cache/save@v4 - id: cache - with: - path: ./node_modules - key: v1-dependencies-${{ env.PACKAGE_CHECKSUM }} + - name: install dependencies + run: npm ci - name: Replace env vars in manifest.yml file run: | sudo apt-get update From d4846e511e48d234fc5c7c5c965ee4c535861239 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Wed, 21 Feb 2024 10:04:06 -0500 Subject: [PATCH 09/11] Add a flag to speed up deploy times --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 889de9e..1260858 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -71,7 +71,7 @@ jobs: 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" + 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 From d7efc9f6c64be9d552f922102ecb63ac83786478 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Mon, 26 Feb 2024 11:34:08 -0500 Subject: [PATCH 10/11] Update README --- README.md | 53 +++++++++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 633b087..82669b0 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,25 @@ -[![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. -# Setup +## 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,22 +39,23 @@ 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 +## Using the API Full API docs can be found here: https://open.gsa.gov/api/dap/ -## Environments +### 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/ -## Overview +### Overview The Analytics API exposes 3 API endpoints: @@ -73,7 +69,7 @@ are found, an empty array is returned. Records are sorted according to the associated date. -### Limit query parameter +#### 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. @@ -85,7 +81,7 @@ set to specify the desired number of records. The maximum number of records that can be rendered for any given request is 10,000. -### Page query parameter +#### 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 @@ -149,7 +145,7 @@ An enum which describes the session. Possible values: 'Direct', 'Organic Search', 'Paid Social', 'Organic Social', 'Email', 'Affiliates', 'Referral', 'Paid Search', 'Video', and 'Display' -# Linting +## Linting This repo uses Eslint and Prettier for code static analysis and formatting. Run the linter with: @@ -164,7 +160,7 @@ Automatically fix lint issues with: npm run lint:fix ``` -# Running the unit tests +## 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 @@ -187,7 +183,7 @@ Run the tests (pre-test hook runs DB migrations): 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 @@ -195,13 +191,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 @@ -230,7 +226,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): From a1380ed769ca8446c0c77a0f6a695adcff734525 Mon Sep 17 00:00:00 2001 From: Michael Levin Date: Mon, 26 Feb 2024 11:50:12 -0500 Subject: [PATCH 11/11] Add links to GA4/UA and deprecation warning --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 82669b0..555a8d3 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,19 @@ A system for publishing data retrieved from the Google Analytics API by the 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. + +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