diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 4a15cf1b8..681973cce 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -22,7 +22,7 @@ jobs: python-version: '3.10' - name: Install ansible and deploy to production run: | - python -m pip install --user ansible-core==2.11.1 + python -m pip install --user ansible-core==2.16.14 cd deploy echo ${{ secrets.ANSIBLE_VAULT_PASSWORD }} > ansible-vault-password.txt ansible-vault view --vault-password-file ansible-vault-password.txt files/jwt-signing-key.pem.enc > ../jwt-signing-key.pem diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index a88d06939..563c5e375 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -22,7 +22,7 @@ jobs: python-version: '3.10' - name: Install ansible and deploy to staging run: | - python -m pip install --user ansible-core==2.11.1 + python -m pip install --user ansible-core==2.16.14 cd deploy echo ${{ secrets.ANSIBLE_VAULT_PASSWORD }} > ansible-vault-password.txt ansible-vault view --vault-password-file ansible-vault-password.txt files/jwt-signing-key.pem.enc > ../jwt-signing-key.pem diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..2312dc587 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/client/components/ManageBotRunDialog/index.jsx b/client/components/ManageBotRunDialog/index.jsx index 8915ad55b..deb7b9163 100644 --- a/client/components/ManageBotRunDialog/index.jsx +++ b/client/components/ManageBotRunDialog/index.jsx @@ -17,6 +17,7 @@ import RetryCanceledCollectionsButton from './RetryCanceledCollectionsButton'; import StopRunningCollectionButton from './StopRunningCollectionButton'; import ViewLogsButton from './ViewLogsButton'; import { TestPlanRunPropType, UserPropType } from '../common/proptypes'; +import { COLLECTION_JOB_STATUS } from '../../utils/collectionJobStatus'; const ManageBotRunDialog = ({ testPlanReportId, @@ -68,6 +69,19 @@ const ManageBotRunDialog = ({ [testers, testPlanReportAssignedTestersQuery] ); + const isBotRunFinished = useMemo(() => { + const status = collectionJobQuery?.collectionJobByTestPlanRunId?.status; + if (!status) return false; + switch (status) { + case COLLECTION_JOB_STATUS.COMPLETED: + case COLLECTION_JOB_STATUS.ERROR: + case COLLECTION_JOB_STATUS.CANCELLED: + return true; + default: + return false; + } + }, [collectionJobQuery]); + const actions = useMemo(() => { return [ { @@ -77,6 +91,7 @@ const ManageBotRunDialog = ({ testPlanRun: testPlanRun, possibleTesters: possibleReassignees, label: 'Assign To ...', + disabled: !isBotRunFinished, onChange } }, diff --git a/client/components/common/AssignTesterDropdown/index.jsx b/client/components/common/AssignTesterDropdown/index.jsx index 5d4cd9ceb..f3e1d3092 100644 --- a/client/components/common/AssignTesterDropdown/index.jsx +++ b/client/components/common/AssignTesterDropdown/index.jsx @@ -28,6 +28,7 @@ const AssignTesterDropdown = ({ possibleTesters, onChange, label, + disabled = false, dropdownAssignTesterButtonRef, setAlertMessage = () => {} }) => { @@ -139,6 +140,7 @@ const AssignTesterDropdown = ({ aria-label="Assign testers" className="assign-tester" variant="secondary" + disabled={disabled} > {renderLabel()} @@ -216,7 +218,8 @@ AssignTesterDropdown.propTypes = { label: PropTypes.string, draftTestPlanRuns: PropTypes.arrayOf(TestPlanRunPropType), setAlertMessage: PropTypes.func, - dropdownAssignTesterButtonRef: PropTypes.object + dropdownAssignTesterButtonRef: PropTypes.object, + disabled: PropTypes.bool }; export default AssignTesterDropdown; diff --git a/deploy/README.md b/deploy/README.md index 41b7a707a..c11cd8dcd 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -24,23 +24,25 @@ machine will not have all the capabilities of the production system. 1. Install [Vagrant](https://www.vagrantup.com/) (version 2) and [VirtualBox](https://www.virtualbox.org/) (version 5.2) 2. Install vagrant-hostsupdater - ``` - vagrant plugin install vagrant-hostsupdater - ``` + ```bash + vagrant plugin install vagrant-hostsupdater + ``` 3. Open a terminal and navigate to the directory containing this text file 4. Run the following command: - ``` - vagrant up - ``` + ```bash + vagrant up + ``` + This will initiate the creation of a virtual machine. You will be prompted for your sudo password. Further documentation on using Vagrant can be found in [the "Getting Started" guide by the maintainers of that project](https://www.vagrantup.com/intro/getting-started/index.html). Once the vagrant box is up you can test by running by going to the ip configured in the `Vagrantfile` [192.168.10.40](192.168.10.40). If you make any changes locally and want to run them again: - ``` - vagrant rsync && vagrant up --provision - ``` + +```bash +vagrant rsync && vagrant up --provision +``` If you want to debug you can run `vagrant ssh` to ssh into the vagrant box. You can view logging from ansible with `sudo -i cat /var/log/messages`. @@ -50,46 +52,52 @@ can view logging from ansible with `sudo -i cat /var/log/messages`. To deploy this project to server: 1. Merge from development to the releases branch as detailed in [docs/release.md](../docs/release.md). -1. Obtain an authorized key and add it to your keychain. This is needed for deploys to Staging and Production. - - The shared key is named `aria-at-bocoup`. - - Place it in the ~/.ssh directory. - - For security, set permissions on the key file, which is required by the OS: `chmod 600 ~/.ssh/aria-at-bocoup`. - - Add it to your keychain with the following command: `ssh-add ~/.ssh/aria-at-bocoup`. - - Add the following `Host` client configuration option to `~/.ssh/config`: - ``` - Host *.w3.internal - ProxyJump bocoupinfra@ssh-aws.w3.org - ``` - - The RSA key fingerprint for `ssh-aws.w3.org` is `SHA256:Nlyly0XFZebw0/k8yCGUXA+Y03W7WB7CPnXiZZb7XE8`. - - Run `ssh root@bestla.w3.internal` (aria-at-staging.w3.org's server) and `ssh root@fenrir.w3.internal` (aria-at.w3.org's server) to verify that you can connect to the servers. - - The RSA key fingerprint for `bestla.w3.internal` is `SHA256:F16aX2Wx4e39jbHhqEkeH8iRgY41C3WgxvAgvh7PQZ0`. - - The RSA key fingerprint for `fenrir.w3.internal` is `SHA256:cF6u/K00P2ELEVbIazVVqqMz5q+Sbh4+Jog/VmXZomg`. +1. Obtain an authorized key and add it to your keychain. This is needed for deploys to Staging and Production. + +- The shared key is named `aria-at-bocoup`. +- Place it in the ~/.ssh directory. +- For security, set permissions on the key file, which is required by the OS: `chmod 600 ~/.ssh/aria-at-bocoup`. +- Add it to your keychain with the following command: `ssh-add ~/.ssh/aria-at-bocoup`. +- Add the following `Host` client configuration option to `~/.ssh/config`: + ``` + Host *.w3.internal + ProxyJump bocoupinfra@ssh-aws.w3.org + ``` + - The RSA key fingerprint for `ssh-aws.w3.org` is `SHA256:Nlyly0XFZebw0/k8yCGUXA+Y03W7WB7CPnXiZZb7XE8`. +- Run `ssh root@bestla.w3.internal` (aria-at-staging.w3.org's server) and `ssh root@fenrir.w3.internal` (aria-at.w3.org's server) to verify that you can connect to the servers. + - The RSA key fingerprint for `bestla.w3.internal` is `SHA256:F16aX2Wx4e39jbHhqEkeH8iRgY41C3WgxvAgvh7PQZ0`. + - The RSA key fingerprint for `fenrir.w3.internal` is `SHA256:cF6u/K00P2ELEVbIazVVqqMz5q+Sbh4+Jog/VmXZomg`. + 1. Bocoup maintains its own instance of the app on its internal infrastructure for quick and easy testing. Note that you must be a Bocouper to deploy to this environment. Follow the steps below to verify you are able to connect. - - Run `ssh aria-at-app-sandbox.bocoup.com` and confirm you can connect. - - Confirm that `sudo su` successfully switches you to the root user. You will need to enter the sudo password you chose during your Bocoup onboarding. This password will be required when deploying to the Sandbox. + +- Run `ssh aria-at-app-sandbox.bocoup.com` and confirm you can connect. +- Confirm that `sudo su` successfully switches you to the root user. You will need to enter the sudo password you chose during your Bocoup onboarding. This password will be required when deploying to the Sandbox. + 1. Obtain a copy of the `ansible-vault-password.txt` file in LastPass and place it in the directory which contains this document. -1. Install [Ansible](https://www.ansible.com/) version 2.11. Instructions for macOS are as follows: - - Install Ansible at the specific 2.11 version: `python3 -m pip install --user ansible-core==2.11.1` - - Add the following line to your `~/.zshrc` file, changing the path below to match where Python installs Ansible for you: - ``` - export PATH=$PATH:/Users/Luigi/Library/Python/3.9/bin - ``` - - Run `source ~/.zshrc` to refresh your shell. - - Install `ansible.posix` to make use of the [ansible.posix.synchronize](https://docs.ansible.com/ansible/latest/collections/ansible/posix/synchronize_module.html#ansible-posix-synchronize-module-a-wrapper-around-rsync-to-make-common-tasks-in-your-playbooks-quick-and-easy) module: `ansible-galaxy collection install ansible.posix` - - Run `ansible --version` to verify your ansible is on version 2.11. +1. Install [Ansible](https://www.ansible.com/) version 2.16. Instructions for macOS are as follows: + +- Install Ansible at the specific 2.16 version: `python3 -m pip install --user ansible-core==2.16.14` +- Add the following line to your `~/.zshrc` file, changing the path below to match where Python installs Ansible for you: + ``` + export PATH=$PATH:/Users/Luigi/Library/Python/3.9/bin + ``` +- Run `source ~/.zshrc` to refresh your shell. +- Install `ansible.posix` to make use of the [ansible.posix.synchronize](https://docs.ansible.com/ansible/latest/collections/ansible/posix/synchronize_module.html#ansible-posix-synchronize-module-a-wrapper-around-rsync-to-make-common-tasks-in-your-playbooks-quick-and-easy) module: `ansible-galaxy collection install ansible.posix` +- Run `ansible --version` to verify your ansible is on version 2.16. + 1. Execute the following command from the deploy directory: - Sandbox: - ``` - ansible-playbook provision.yml --inventory inventory/sandbox.yml - ``` + ``` + ansible-playbook provision.yml --inventory inventory/sandbox.yml + ``` - Staging: - ``` - ansible-playbook provision.yml --inventory inventory/staging.yml - ``` + ``` + ansible-playbook provision.yml --inventory inventory/staging.yml + ``` - Production: - ``` - ansible-playbook provision.yml --inventory inventory/production.yml - ``` + ``` + ansible-playbook provision.yml --inventory inventory/production.yml + ``` ## Environment Configuration @@ -102,25 +110,28 @@ ansible-vault edit files/config-sandbox.env ``` ## Manual DB Backup + From the `deploy` folder: 1. Retrieve the database user (aka PGUSER) and database password (aka PGPASSWORD), the environment is `production` or `staging`. `ansible-vault view --vault-password-file ansible-vault-password.txt files/config-.env` 2. Ssh into the machine. The deploy key will be at `~/.ssh/aria-at-bocoup`, if you followed the deploy setup instructions. The domain is `aria-at.w3.org` for production and `aria-at-staging.w3.org` for staging. - `ssh -i root@` + `ssh -i root@` 3. Create the backup and save it to a file. After running the command, the terminal prompt will ask for the PGPASSWORD. Use the current date for the timestamp, e.g. `20230406`. Run - `pg_dump -U -h localhost -d aria_at_report > _dump_.sql`. + `pg_dump -U -h localhost -d aria_at_report > _dump_.sql`. 4. From another terminal window that's not connected to the server, copy the backup to your machine. - `scp -i root@:_dump_.sql .` + `scp -i root@:_dump_.sql .` ## Database Restore + 1. Ssh into the machine. - `ssh -i root@aria-at.w3.org` + `ssh -i root@aria-at.w3.org` 2. Load the backup that was created - `psql -d aria_at_report -f _dump_.sql` + `psql -d aria_at_report -f _dump_.sql` ## Github Workflow Automation Configuration -* The `jwt-signing-key.pem` file should be located in the project root folder. + +- The `jwt-signing-key.pem` file should be located in the project root folder. From within the `deploy` folder, you can run `ansible-vault view --vault-password-file ansible-vault-password.txt files/jwt-signing-key.pem.enc > ../jwt-signing-key.pem` to decrypt the configuration file. -* The `AUTOMATION_CALLBACK_FQDN` environment variable in the environment configuration file should be a **fully qualified domain name** that is accessible from the github workflow server pointing at the running instance of aria-at-app. For local development testing of these features, a forwarding proxy server like `ngrok` is recommended: `npx ngrok http 3000 --host-header=rewrite` will setup a server forwarding to your local 3000 development port. You can then use the domain it gives you when launching the app: +- The `AUTOMATION_CALLBACK_FQDN` environment variable in the environment configuration file should be a **fully qualified domain name** that is accessible from the github workflow server pointing at the running instance of aria-at-app. For local development testing of these features, a forwarding proxy server like `ngrok` is recommended: `npx ngrok http 3000 --host-header=rewrite` will setup a server forwarding to your local 3000 development port. You can then use the domain it gives you when launching the app: `AUTOMATION_CALLBACK_FQDN=128935b17294.ngrok.app yarn dev` diff --git a/deploy/provision.yml b/deploy/provision.yml index a44ca3bf3..64d182c47 100644 --- a/deploy/provision.yml +++ b/deploy/provision.yml @@ -2,7 +2,7 @@ - hosts: all become_method: sudo pre_tasks: - - include: tasks/prompt-for-become-password.yml + - import_tasks: tasks/prompt-for-become-password.yml when: needs_become_password|default(false, true) roles: - permissions diff --git a/deploy/roles/application/tasks/cron.yml b/deploy/roles/application/tasks/cron.yml index 294b19f4e..40d14fb0d 100644 --- a/deploy/roles/application/tasks/cron.yml +++ b/deploy/roles/application/tasks/cron.yml @@ -1,15 +1,52 @@ --- # Create a cron to import the most recent tests from aria-at -- name: Set a cron job to build test results +- name: Set a cron job to build and import latest test versions from aria-at cron: name: "import latest aria-at tests" minute: "15" job: "curl -X POST https://{{fqdn}}/api/test/import" - when: deployment_mode != 'development' + when: deployment_mode != 'development' -- name: Set a cron job to build test results in development +- name: Set a cron job to build and import latest test versions from aria-at in development cron: name: "import latest aria-at tests" minute: "15" job: "curl -X POST http://localhost:5000/api/test/import" when: deployment_mode == 'development' + +- name: Ensure proper permissions for application_user on db_dumps directory + become: yes + block: + - name: Ensure the db_dumps directory exists + file: + path: /home/{{application_user}}/db_dumps + state: directory + owner: '{{application_user}}' + group: '{{application_user}}' + mode: '0755' + + - name: Ensure application_user has write permissions on the db_dumps directory + file: + path: /home/{{application_user}}/db_dumps + owner: '{{application_user}}' + group: '{{application_user}}' + mode: '0775' + when: deployment_mode == 'staging' or deployment_mode == 'production' + +# Create a cron to dump the database in staging and production (run every day at 00:00) +- name: Set a cron job to create a new database dump + cron: + name: "create new database dump" + hour: "0" + minute: "0" + job: "curl -X POST https://{{fqdn}}/api/database/dump" + when: deployment_mode == 'staging' or deployment_mode == 'production' + +# Create a cron to clean up the database dumps folder in staging and production (run every day at 00:05) +- name: Set a cron job to clean up the database dumps folder + cron: + name: "clean up the database dumps folder if necessary" + hour: "0" + minute: "5" + job: "curl -X POST https://{{fqdn}}/api/database/cleanFolder" + when: deployment_mode == 'staging' or deployment_mode == 'production' diff --git a/deploy/roles/application/tasks/main.yml b/deploy/roles/application/tasks/main.yml index db5b9b7ff..ef6249408 100644 --- a/deploy/roles/application/tasks/main.yml +++ b/deploy/roles/application/tasks/main.yml @@ -8,7 +8,7 @@ set_fact: source_dir: /home/{{application_user}}/aria-at-report -- include: upload-source-code.yml +- include_tasks: upload-source-code.yml - name: Allow aria-bot user to run import script as admin on sandbox lineinfile: @@ -28,7 +28,7 @@ recurse: yes become: yes when: deployment_mode != 'development' - notify: "restart server" + notify: 'restart server' - name: Make server resources folder writable for import tests API endpoint file: @@ -37,7 +37,7 @@ recurse: yes become: yes when: deployment_mode != 'development' - notify: "restart server" + notify: 'restart server' - name: Make client resources folder writable for import harness file: @@ -46,7 +46,7 @@ recurse: yes become: yes when: deployment_mode != 'development' - notify: "restart server" + notify: 'restart server' - name: Link application code file: @@ -108,6 +108,6 @@ args: chdir: '{{source_dir}}' -- include: service.yml +- import_tasks: service.yml -- include: cron.yml +- include_tasks: cron.yml diff --git a/deploy/roles/nodejs/tasks/main.yml b/deploy/roles/nodejs/tasks/main.yml index e8d6c538c..91ff64933 100644 --- a/deploy/roles/nodejs/tasks/main.yml +++ b/deploy/roles/nodejs/tasks/main.yml @@ -8,7 +8,7 @@ - https://dl.yarnpkg.com/debian/pubkey.gpg - https://deb.nodesource.com/gpgkey/nodesource.gpg.key -- include: upgrade.yml +- include_tasks: upgrade.yml - name: Add software repositories apt_repository: diff --git a/package.json b/package.json index 6c945b975..ba5ba67c9 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "storybook": "yarn workspaces run storybook", "sequelize": "dotenv -e config/dev.env npx sequelize-cli", "sequelize:test": "dotenv -e config/test.env npx sequelize-cli", - "postinstall": "patch-package" + "postinstall": "patch-package", + "prepare": "husky" }, "repository": { "type": "git", @@ -44,6 +45,23 @@ "postinstall-postinstall": "^2.1.0" }, "devDependencies": { + "husky": "^9.1.7", + "lint-staged": "^15.3.0", "npm-run-all": "^4.1.5" + }, + "lint-staged": { + "*": "prettier --ignore-unknown --write", + "client/*": [ + "yarn workspace client prettier", + "yarn workspace client lint" + ], + "server/*": [ + "yarn workspace server prettier", + "yarn workspace server lint" + ], + "shared/*": [ + "yarn workspace shared prettier", + "yarn workspace shared lint" + ] } } diff --git a/server/app.js b/server/app.js index 90b3217b5..36737e3b7 100644 --- a/server/app.js +++ b/server/app.js @@ -8,6 +8,7 @@ const authRoutes = require('./routes/auth'); const testRoutes = require('./routes/tests'); const transactionRoutes = require('./routes/transactions'); const automationSchedulerRoutes = require('./routes/automation'); +const databaseRoutes = require('./routes/database'); const path = require('path'); const apolloServer = require('./graphql-server'); const { @@ -24,6 +25,7 @@ app.use('/auth', authRoutes); app.use('/test', testRoutes); app.use('/transactions', transactionRoutes); app.use('/jobs', automationSchedulerRoutes); +app.use('/database', databaseRoutes); apolloServer.start().then(() => { apolloServer.applyMiddleware({ app }); diff --git a/server/controllers/DatabaseController.js b/server/controllers/DatabaseController.js new file mode 100644 index 000000000..2416d4a69 --- /dev/null +++ b/server/controllers/DatabaseController.js @@ -0,0 +1,289 @@ +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { Client } = require('pg'); +const { exec } = require('child_process'); +const { dates } = require('shared'); + +// Database configuration +const DB_NAME = process.env.PGDATABASE; +const DB_USER = process.env.PGUSER; +const DB_PASSWORD = process.env.PGPASSWORD; +const DB_HOST = process.env.PGHOST; + +// To avoid unintentionally exposing environment variables in error messages +const sanitizeError = message => { + const redacted = '#redacted#'; + + const dbNameRegex = new RegExp(DB_NAME, 'g'); + const dbUserRegex = new RegExp(DB_USER, 'g'); + const dbPasswordRegex = new RegExp(DB_PASSWORD, 'g'); + const dbHostRegex = new RegExp(DB_HOST, 'g'); + + return message + .replace(dbNameRegex, redacted) + .replace(dbUserRegex, redacted) + .replace(dbPasswordRegex, redacted) + .replace(dbHostRegex, redacted); +}; + +const dumpPostgresDatabaseToFile = (dumpFileName = null) => { + const OUTPUT_DIR = path.join(os.homedir(), 'db_dumps'); + const DUMP_FILE = path.join( + OUTPUT_DIR, + dumpFileName || + `${process.env.ENVIRONMENT}_dump_${dates.convertDateToString( + new Date(), + 'YYYYMMDD_HHmmss' + )}.sql` + ); + + return new Promise((resolve, reject) => { + try { + // pg_dump command that ignores the table "session" because it causes unnecessary conflicts when being used to restore a db + const pgDumpCommand = `PGPASSWORD=${DB_PASSWORD} pg_dump -U ${DB_USER} -h ${DB_HOST} -d ${DB_NAME} --exclude-table=session > ${DUMP_FILE}`; + exec(pgDumpCommand, (error, stdout, stderr) => { + if (error) { + return reject( + new Error( + `Error executing pg_dump: ${sanitizeError(error.message)}` + ) + ); + } + if (stderr) { + return reject(new Error(`pg_dump stderr: ${sanitizeError(stderr)}`)); + } + return resolve(`Database dump completed successfully: ${DUMP_FILE}`); + }); + } catch (error) { + return reject( + new Error(`Unable to dump database: ${sanitizeError(error.message)}`) + ); + } + }); +}; + +const removeDatabaseTablesAndFunctions = async res => { + const client = new Client({ + user: DB_USER, + host: DB_HOST, + database: DB_NAME, + password: DB_PASSWORD + }); + + try { + await client.connect(); + res.write('Connected to the database.\n'); + + // [Tables DROP // START] + // Get all tables in the public schema + const tablesQuery = ` + SELECT tablename + FROM pg_tables + WHERE schemaname = 'public'; + `; + const tablesResult = await client.query(tablesQuery); + + // Execute DROP TABLE commands + const tableNames = tablesResult.rows + // Ignore 'session' because dropping it may cause unnecessary conflicts + // when restoring + .filter(row => row.tablename !== 'session') + .map(row => `"${row.tablename}"`); + if (tableNames.length === 0) { + res.write('No tables found to drop.\n'); + } else { + // eslint-disable-next-line no-console + console.info(`Dropping tables: ${tableNames.join(', ')}`); + await client.query(`DROP TABLE ${tableNames.join(', ')} CASCADE;`); + res.write('All tables have been dropped successfully.\n'); + } + // [Tables DROP // END] + + // [Functions DROP // START] + // Get all user-defined functions + const functionsQuery = ` + SELECT 'DROP FUNCTION IF EXISTS ' || n.nspname || '.' || p.proname || '(' || + pg_get_function_identity_arguments(p.oid) || ');' AS drop_statement + FROM pg_proc p + JOIN pg_namespace n ON p.pronamespace = n.oid + WHERE n.nspname NOT IN ('pg_catalog', 'information_schema'); + `; + + const functionsResult = await client.query(functionsQuery); + const dropStatements = functionsResult.rows.map(row => row.drop_statement); + + // Execute each DROP FUNCTION statement + for (const dropStatement of dropStatements) { + // eslint-disable-next-line no-console + console.info(`Executing: ${dropStatement}`); + await client.query(dropStatement); + } + res.write('All functions removed successfully!\n'); + // [Functions DROP // END] + } catch (error) { + res.write( + `Error removing tables or functions: ${sanitizeError(error.message)}\n` + ); + } finally { + await client.end(); + res.write('Disconnected from the database.\n'); + } +}; + +const dumpPostgresDatabase = async (req, res) => { + const { dumpFileName } = req.body; + + if (dumpFileName && path.extname(dumpFileName) !== '.sql') + return res.status(400).send("Provided file name does not include '.sql'"); + + try { + const result = await dumpPostgresDatabaseToFile(); + return res.status(200).send(result); + } catch (error) { + return res.status(400).send(error.message); + } +}; + +const restorePostgresDatabase = async (req, res) => { + // Prevent unintentionally dropping or restoring database tables if ran on + // production unless manually done + if ( + process.env.ENVIRONMENT === 'production' || + process.env.API_SERVER === 'https://aria-at.w3.org' || + req.hostname.includes('aria-at.w3.org') + ) { + return res.status(405).send('This request is not permitted'); + } + + const { pathToFile } = req.body; + if (!pathToFile) + return res.status(400).send("'pathToFile' is missing. Please provide."); + + if (path.extname(pathToFile) !== '.sql') + return res + .status(400) + .send("The provided path is not in the expected '.sql' format."); + + // Backup current db before running restore in case there is a need to revert + const dumpFileName = `${ + process.env.ENVIRONMENT + }_dump_${dates.convertDateToString( + new Date(), + 'YYYYMMDD_HHmmss' + )}_before_restore.sql`; + + try { + const result = await dumpPostgresDatabaseToFile(dumpFileName); + res.status(200).write(`${result}\n\n`); + } catch (error) { + return res + .status(400) + .send( + `Unable to continue restore. Failed to backup current data:\n${error.message}` + ); + } + + // Purge the database's tables and functions to make restoring with the + // pg import easier + await removeDatabaseTablesAndFunctions(res); + + try { + const pgImportCommand = `PGPASSWORD=${DB_PASSWORD} psql -U ${DB_USER} -h ${DB_HOST} -d ${DB_NAME} -f ${pathToFile}`; + + // Execute the command + exec(pgImportCommand, (error, stdout, stderr) => { + if (error) { + res + .status(400) + .write(`Error executing pg import: ${sanitizeError(error.message)}`); + } + if (stderr) { + res.status(400).write(`pg import stderr:: ${sanitizeError(stderr)}`); + } + res.status(200).write(`Database import completed successfully`); + return res.end(); + }); + } catch (error) { + res + .status(400) + .write(`Unable to import database: ${sanitizeError(error.message)}`); + return res.end(); + } +}; + +const cleanFolder = (req, res) => { + const { maxFiles } = req.body; + + if (maxFiles) { + if (!isNaN(maxFiles) && Number.isInteger(Number(maxFiles))) { + // continue + } else { + return res + .status(400) + .send("Unable to parse the 'maxFiles' value provided."); + } + } else { + // continue + } + + const CLEAN_DIR = path.join(os.homedir(), 'db_dumps'); + // Default the number of max files to 28 if no value provided + const MAX_FILES = Number(maxFiles) || 28; + + if ( + process.env.ENVIRONMENT === 'production' || + process.env.API_SERVER === 'https://aria-at.w3.org' || + req.hostname.includes('aria-at.w3.org') + ) { + if (!CLEAN_DIR.includes('/home/aria-bot/db_dumps')) { + return res + .status(500) + .send("Please ensure the 'db_dumps' folder is properly set."); + } + } + + try { + // Read all files in the folder + const files = fs.readdirSync(CLEAN_DIR).map(file => { + const filePath = path.join(CLEAN_DIR, file); + return { + name: file, + path: filePath, + time: fs.statSync(filePath).mtime.getTime() + }; + }); + + files.sort((a, b) => a.time - b.time); + + // Delete files if there are more than maxFiles + if (files.length > MAX_FILES) { + const removedFiles = []; + + const filesToDelete = files.slice(0, files.length - MAX_FILES); + filesToDelete.forEach(file => { + fs.unlinkSync(file.path); + removedFiles.push(file); + }); + return res + .status(200) + .send( + `Removed the following files:\n${removedFiles + .map(({ name }, index) => `#${index + 1}) ${name}`) + .join('\n')}` + ); + } else { + return res + .status(200) + .send('No files to delete. Folder is within the limit.'); + } + } catch (error) { + return res.status(400).send(`Error cleaning folder: ${error.message}`); + } +}; + +module.exports = { + dumpPostgresDatabase, + restorePostgresDatabase, + cleanFolder +}; diff --git a/server/routes/database.js b/server/routes/database.js new file mode 100644 index 000000000..9000f27e5 --- /dev/null +++ b/server/routes/database.js @@ -0,0 +1,14 @@ +const { Router } = require('express'); +const { + dumpPostgresDatabase, + restorePostgresDatabase, + cleanFolder +} = require('../controllers/DatabaseController'); + +const router = Router(); + +router.post('/dump', dumpPostgresDatabase); +router.post('/restore', restorePostgresDatabase); +router.post('/cleanFolder', cleanFolder); + +module.exports = router; diff --git a/yarn.lock b/yarn.lock index 0507e6935..f703e789c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5155,6 +5155,13 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.21.3" +ansi-escapes@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.0.0.tgz#00fc19f491bbb18e1d481b97868204f92109bfe7" + integrity sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw== + dependencies: + environment "^1.0.0" + ansi-html-community@0.0.8, ansi-html-community@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" @@ -5204,7 +5211,7 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -ansi-styles@^6.1.0: +ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== @@ -6189,6 +6196,13 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -6547,6 +6561,11 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@~5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" + integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -6732,6 +6751,13 @@ cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" +cli-cursor@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-5.0.0.tgz#24a4831ecf5a6b01ddeb32fb71a4b2088b0dce38" + integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw== + dependencies: + restore-cursor "^5.0.0" + cli-table3@^0.6.1: version "0.6.3" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" @@ -6741,6 +6767,14 @@ cli-table3@^0.6.1: optionalDependencies: "@colors/colors" "1.5.0" +cli-truncate@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-4.0.0.tgz#6cc28a2924fee9e25ce91e973db56c7066e6172a" + integrity sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== + dependencies: + slice-ansi "^5.0.0" + string-width "^7.0.0" + cli-width@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" @@ -6849,6 +6883,11 @@ colorette@^2.0.10, colorette@^2.0.14: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== +colorette@^2.0.20: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -6886,6 +6925,11 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== +commander@~12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + common-path-prefix@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" @@ -7511,6 +7555,13 @@ debug@^4.3.5, debug@^4.3.6: dependencies: ms "2.1.2" +debug@~4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -8038,6 +8089,11 @@ emittery@^0.13.1: resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== +emoji-regex@^10.3.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4" + integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -8131,6 +8187,11 @@ envinfo@^7.7.3: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== +environment@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/environment/-/environment-1.1.0.tgz#8e86c66b180f363c7ab311787e0259665f45a9f1" + integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== + errno@^0.1.3, errno@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -8719,6 +8780,11 @@ eventemitter3@^4.0.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + events@^3.0.0, events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -8747,6 +8813,21 @@ execa@^5.0.0, execa@^5.1.1: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +execa@~8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^8.0.1" + human-signals "^5.0.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^4.1.0" + strip-final-newline "^3.0.0" + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -9136,6 +9217,13 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + finalhandler@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" @@ -9511,6 +9599,11 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-east-asian-width@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389" + integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== + get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" @@ -9553,6 +9646,11 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + get-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" @@ -10270,6 +10368,16 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + +husky@^9.1.7: + version "9.1.7" + resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d" + integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== + hyphenate-style-name@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" @@ -10732,6 +10840,18 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-fullwidth-code-point@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" + integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + +is-fullwidth-code-point@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz#9609efced7c2f97da7b60145ef481c787c7ba704" + integrity sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA== + dependencies: + get-east-asian-width "^1.0.0" + is-function@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.2.tgz#4f097f30abf6efadac9833b17ca5dc03f8144e08" @@ -10886,6 +11006,11 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -11885,11 +12010,44 @@ lighthouse@9.6.8: yargs "^17.3.1" yargs-parser "^21.0.0" +lilconfig@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" + integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +lint-staged@^15.3.0: + version "15.3.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.3.0.tgz#32a0b3f2f2b8825950bd3b9fb093e045353bdfa3" + integrity sha512-vHFahytLoF2enJklgtOtCtIjZrKD/LoxlaUusd5nh7dWv/dkKQJY74ndFSzxCdv7g0ueGg1ORgTSt4Y9LPZn9A== + dependencies: + chalk "~5.4.1" + commander "~12.1.0" + debug "~4.4.0" + execa "~8.0.1" + lilconfig "~3.1.3" + listr2 "~8.2.5" + micromatch "~4.0.8" + pidtree "~0.6.0" + string-argv "~0.3.2" + yaml "~2.6.1" + +listr2@~8.2.5: + version "8.2.5" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.5.tgz#5c9db996e1afeb05db0448196d3d5f64fec2593d" + integrity sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ== + dependencies: + cli-truncate "^4.0.0" + colorette "^2.0.20" + eventemitter3 "^5.0.1" + log-update "^6.1.0" + rfdc "^1.4.1" + wrap-ansi "^9.0.0" + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -12028,6 +12186,17 @@ lodash@^4.17.10, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-update@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.1.0.tgz#1a04ff38166f94647ae1af562f4bd6a15b1b7cd4" + integrity sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w== + dependencies: + ansi-escapes "^7.0.0" + cli-cursor "^5.0.0" + slice-ansi "^7.1.0" + strip-ansi "^7.1.0" + wrap-ansi "^9.0.0" + loglevel@^1.6.8: version "1.8.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4" @@ -12371,6 +12540,14 @@ micromatch@^4.0.2, micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" +micromatch@~4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -12426,6 +12603,16 @@ mimic-fn@^3.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + +mimic-function@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" + integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== + min-document@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" @@ -12617,7 +12804,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -12905,6 +13092,13 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +npm-run-path@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" + integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== + dependencies: + path-key "^4.0.0" + npmlog@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" @@ -13149,6 +13343,20 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + +onetime@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-7.0.0.tgz#9f16c92d8c9ef5120e3acd9dd9957cceecc1ab60" + integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== + dependencies: + mimic-function "^5.0.0" + open@^7.0.3, open@^7.1.0, open@^7.4.2: version "7.4.2" resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" @@ -13551,6 +13759,11 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" @@ -13727,6 +13940,11 @@ pidtree@^0.3.0: resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a" integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA== +pidtree@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" + integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -15001,6 +15219,14 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" +restore-cursor@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-5.1.0.tgz#0766d95699efacb14150993f55baf0953ea1ebe7" + integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== + dependencies: + onetime "^7.0.0" + signal-exit "^4.1.0" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -15021,6 +15247,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfdc@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + rimraf@3.0.2, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -15615,7 +15846,7 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.0.1: +signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -15647,6 +15878,22 @@ slash@^4.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== +slice-ansi@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" + integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== + dependencies: + ansi-styles "^6.0.0" + is-fullwidth-code-point "^4.0.0" + +slice-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" + integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== + dependencies: + ansi-styles "^6.2.1" + is-fullwidth-code-point "^5.0.0" + smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" @@ -15978,6 +16225,11 @@ streamx@^2.15.0, streamx@^2.18.0: optionalDependencies: bare-events "^2.2.0" +string-argv@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" + integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -16021,6 +16273,15 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string-width@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + "string.prototype.matchall@^4.0.0 || ^3.0.1": version "4.0.8" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" @@ -16193,7 +16454,7 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.0.1: +strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== @@ -16222,6 +16483,11 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" @@ -17947,6 +18213,15 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" +wrap-ansi@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e" + integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== + dependencies: + ansi-styles "^6.2.1" + string-width "^7.0.0" + strip-ansi "^7.1.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -18075,6 +18350,11 @@ yaml@^2.2.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e" integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg== +yaml@~2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773" + integrity sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg== + yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"