From 792e6ee7b3727ec7df9b0a780b3afb77fdd73dbb Mon Sep 17 00:00:00 2001 From: Peter Harrison <16875803+palisadoes@users.noreply.github.com> Date: Fri, 21 Feb 2025 21:41:16 -0800 Subject: [PATCH] Revert "Sample Data Loading and Test in GitHub Action (#3223)" (#3271) This reverts commit 4ff9d91c0cb8e179486c1b08224f62ba020239bb. --- .github/workflows/pull-request.yml | 18 + docker/api.Containerfile | 3 - docs/docs/docs/developer-resources/testing.md | 54 +-- .../docs/docs/getting-started/installation.md | 27 +- package.json | 6 +- pnpm-lock.yaml | 358 +++++++++++++++ .../organization_memberships.json | 0 .../organizations.json | 0 .../sample_data => sample_data}/users.json | 0 scripts/dbManagement/addSampleData.ts | 70 --- scripts/dbManagement/helpers.ts | 421 ------------------ scripts/dbManagement/resetDB.ts | 76 ---- src/utilities/loadSampleData.ts | 305 +++++++++++++ .../dbManagement/addSampleData.test.ts | 127 ------ test/scripts/dbManagement/helpers.test.ts | 252 ----------- test/scripts/dbManagement/resetDB.test.ts | 131 ------ vitest.config.ts | 2 +- 17 files changed, 697 insertions(+), 1153 deletions(-) rename {scripts/dbManagement/sample_data => sample_data}/organization_memberships.json (100%) rename {scripts/dbManagement/sample_data => sample_data}/organizations.json (100%) rename {scripts/dbManagement/sample_data => sample_data}/users.json (100%) delete mode 100644 scripts/dbManagement/addSampleData.ts delete mode 100644 scripts/dbManagement/helpers.ts delete mode 100644 scripts/dbManagement/resetDB.ts create mode 100644 src/utilities/loadSampleData.ts delete mode 100644 test/scripts/dbManagement/addSampleData.test.ts delete mode 100644 test/scripts/dbManagement/helpers.test.ts delete mode 100644 test/scripts/dbManagement/resetDB.test.ts diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 5de7556adaa..05898623439 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -302,3 +302,21 @@ jobs: run: | source venv/bin/activate python .github/workflows/scripts/check_docstrings.py --directories .github + + + # Import-Sample-Data: + # name: Import Sample Data + # runs-on: ubuntu-latest + # steps: + # - name: Checkout this repository + # uses: actions/checkout@v4.2.2 + # - name: Create .env file for talawa api testing environment + # run: cp ./envFiles/.env.ci ./.env + # - name: Build talawa api non production environment docker image + # run: docker compose build + # - name: Run import:sample-data + # uses: addnab/docker-run-action@v3 + # with: + # image: talawa_api + # options: --env-file ./envFiles/.env.ci + # run: pnpm run import:sample-data diff --git a/docker/api.Containerfile b/docker/api.Containerfile index 3e908a35500..63c96d7a2da 100644 --- a/docker/api.Containerfile +++ b/docker/api.Containerfile @@ -37,9 +37,6 @@ USER talawa RUN curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell \ # Appends the fnm configuration to `/home/talawa/.bashrc` file. && echo eval \"\$\(fnm env --corepack-enabled --resolve-engines --use-on-cd --version-file-strategy=recursive\)\" >> /home/talawa/.bashrc -# Set the PATH for all shells -ENV NODE_VERSION=23.7.0 -ENV PATH="/home/talawa/.local/share/fnm/node-versions/v${NODE_VERSION}/installation/bin:/home/talawa/.local/share/fnm/node-versions/v${NODE_VERSION}/installation/lib/node_modules/corepack/shims:${PATH}" ENV PATH=/home/talawa/.local/share/fnm:${PATH} WORKDIR /home/talawa/api diff --git a/docs/docs/docs/developer-resources/testing.md b/docs/docs/docs/developer-resources/testing.md index 99e77483459..f0f5c6b62f5 100644 --- a/docs/docs/docs/developer-resources/testing.md +++ b/docs/docs/docs/developer-resources/testing.md @@ -10,10 +10,9 @@ This section covers important tests to validate the operation of the API. ### Sample Database Login Credentials If the API: - 1. is running with an unmodified `.env` file copied from `envFiles/.env.devcontainer` and; 2. the API sample database is loaded; - then you can use these login credentials to access the API via various clients. +then you can use these login credentials to access the API via various clients. | Email | Password | User Type | Joined Organization | | -----------------------------------| -------- | ---------------| -------------------- | @@ -187,57 +186,6 @@ CloudBeaver is a lightweight web application designed for comprehensive data man 6. You should now see the `PostgreSql@postgres-test` connection in the list of available connections. Click on the connection to open the database. 7. Navigate to `PostgreSql@postgres-test > Databases > talawa > Schemas > public > Tables` to view the available tables. -## Resetting Database (Interactive) - -**NOTE:** This applies only to Talawa API developers. - -Sometimes you may want to start all over again from scratch. These steps ensure all tables emptied and the default administrator account automatically restored. - -### Using the CLI (Dev Containers) - -This applies to users running Talawa API in dev containers. - -1. Once the server is running, open a new terminal session. - -2. Open a bash session inside the running container: - - ```bash - docker exec -it talawa-api-1 /bin/bash - ``` - -3. **WARNING:** This command will **DELETE** all data from each table in your database, administrator roles will be restored. Use with extreme caution. - - ```bash - pnpm run reset:db - ``` - -4. This command will add sample data to make it easier for developers to get an understanding of the application. - - ```bash - pnpm run add:sample_data - ``` - -5. Then exit - - ```bash - exit - ``` - -### Using VS Code Dev Container - -This applies to users running Talawa API in dev containers and VSCode. - -1. **WARNING:** This command will **DELETE** all data from each table in your database, administrator roles will be restored. Use with extreme caution. - ```bash - pnpm run reset:db - ``` -2. This command will add sample data to make it easier for developers to get an understanding of the application. - ```bash - pnpm run add:sample_data - ``` - -Now you can resume your development work. - ## Object Storage Management MinIO is a free, open-source object storage server that's compatible with Amazon S3. It's designed for large-scale data storage and can run on-premises or in the cloud. diff --git a/docs/docs/docs/getting-started/installation.md b/docs/docs/docs/getting-started/installation.md index 5e59cef8719..ba553c7c27a 100644 --- a/docs/docs/docs/getting-started/installation.md +++ b/docs/docs/docs/getting-started/installation.md @@ -387,31 +387,24 @@ You can run the app after closing the terminal or restating the vscode using the We have created sample data to make it easier for end users to get an understanding of the application. -### Using the CLI (Dev Containers) +### Importing Sample Data This applies to users running Talawa API in dev containers. 1. Once the server is running, open a new terminal session. - -2. Run the following command to import sample data into the database: - +2. Open a bash session inside the running container: ```bash - docker exec talawa-api-1 /bin/bash -c 'pnpm run add:sample_data && exit' + docker exec -it talawa-api-1 /bin/bash ``` - - Refer to the next section for login information. - -### Using VS Code Dev Container - -This applies to users running Talawa API in dev containers and VSCode. - -1. Open a terminal inside the container. - -2. Run the following command to import sample data into the database: - +3. Inside the container, run the following command to import sample data into the database: ```bash - pnpm run add:sample_data + pnpm run import:sample-data ``` +4. Then exit + ```bash + exit + ``` + Refer to the next section for login information. ### Sample Data Users diff --git a/package.json b/package.json index 51c25b9f62f..c13053a490b 100644 --- a/package.json +++ b/package.json @@ -17,17 +17,20 @@ "close-with-grace": "^2.2.0", "drizzle-orm": "^0.39.1", "drizzle-zod": "0.6.1", + "dotenv": "^16.0.3", "env-schema": "^6.0.1", "fastify": "^5.2.1", "fastify-plugin": "^5.0.1", "graphql": "^16.10.0", "graphql-scalars": "^1.24.0", "graphql-upload-minimal": "^1.6.1", + "inquirer": "^12.4.1", "mercurius": "^16.0.1", "mercurius-upload": "^8.0.0", "minio": "^8.0.4", "postgres": "^3.4.5", "ulidx": "^2.4.1", + "uuid": "^11.0.5", "uuidv7": "^1.0.2", "zod": "^3.24.1" }, @@ -85,8 +88,7 @@ "generate_drizzle_migrations": "drizzle-kit generate", "generate_graphql_sdl_file": "tsx ./scripts/generateGraphQLSDLFile.ts", "generate_gql_tada": "gql.tada generate-output && gql.tada turbo --fail-on-warn", - "reset:db": "tsx ./scripts/dbManagement/resetDB.ts", - "add:sample_data": "tsx ./scripts/dbManagement/addSampleData.ts", + "import:sample-data": "tsx ./src/utilities/loadSampleData.ts", "push_drizzle_schema": "drizzle-kit push", "push_drizzle_test_schema": "drizzle-kit push --config=./test/drizzle.config.ts", "run_tests": "vitest --coverage", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7fee19a527b..808f8b18198 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: close-with-grace: specifier: ^2.2.0 version: 2.2.0 + dotenv: + specifier: ^16.0.3 + version: 16.4.7 drizzle-orm: specifier: ^0.39.1 version: 0.39.2(postgres@3.4.5) @@ -65,6 +68,9 @@ importers: graphql-upload-minimal: specifier: ^1.6.1 version: 1.6.1(graphql@16.10.0) + inquirer: + specifier: ^12.4.1 + version: 12.4.1(@types/node@22.13.1) mercurius: specifier: ^16.0.1 version: 16.0.1(graphql@16.10.0) @@ -80,6 +86,9 @@ importers: ulidx: specifier: ^2.4.1 version: 2.4.1 + uuid: + specifier: ^11.0.5 + version: 11.0.5 uuidv7: specifier: ^1.0.2 version: 1.0.2 @@ -899,6 +908,127 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@inquirer/checkbox@4.1.1': + resolution: {integrity: sha512-os5kFd/52gZTl/W6xqMfhaKVJHQM8V/U1P8jcSaQJ/C4Qhdrf2jEXdA/HaxfQs9iiUA/0yzYhk5d3oRHTxGDDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.5': + resolution: {integrity: sha512-ZB2Cz8KeMINUvoeDi7IrvghaVkYT2RB0Zb31EaLWOE87u276w4wnApv0SH2qWaJ3r0VSUa3BIuz7qAV2ZvsZlg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.1.6': + resolution: {integrity: sha512-Bwh/Zk6URrHwZnSSzAZAKH7YgGYi0xICIBDFOqBQoXNNAzBHw/bgXgLmChfp+GyR3PnChcTbiCTZGC6YJNJkMA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.6': + resolution: {integrity: sha512-l0smvr8g/KAVdXx4I92sFxZiaTG4kFc06cFZw+qqwTirwdUHMFLnouXBB9OafWhpO3cfEkEz2CdPoCmor3059A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.8': + resolution: {integrity: sha512-k0ouAC6L+0Yoj/j0ys2bat0fYcyFVtItDB7h+pDFKaDDSFJey/C/YY1rmIOqkmFVZ5rZySeAQuS8zLcKkKRLmg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.10': + resolution: {integrity: sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw==} + engines: {node: '>=18'} + + '@inquirer/input@4.1.5': + resolution: {integrity: sha512-bB6wR5wBCz5zbIVBPnhp94BHv/G4eKbUEjlpCw676pI2chcvzTx1MuwZSCZ/fgNOdqDlAxkhQ4wagL8BI1D3Zg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.8': + resolution: {integrity: sha512-CTKs+dT1gw8dILVWATn8Ugik1OHLkkfY82J+Musb57KpmF6EKyskv8zmMiEJPzOnLTZLo05X/QdMd8VH9oulXw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.8': + resolution: {integrity: sha512-MgA+Z7o3K1df2lGY649fyOBowHGfrKRz64dx3+b6c1w+h2W7AwBoOkHhhF/vfhbs5S4vsKNCuDzS3s9r5DpK1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.3.1': + resolution: {integrity: sha512-r1CiKuDV86BDpvj9DRFR+V+nIjsVBOsa2++dqdPqLYAef8kgHYvmQ8ySdP/ZeAIOWa27YGJZRkENdP3dK0H3gg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.0.8': + resolution: {integrity: sha512-hl7rvYW7Xl4un8uohQRUgO6uc2hpn7PKqfcGkCOWC0AA4waBxAv6MpGOFCEDrUaBCP+pXPVqp4LmnpWmn1E1+g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.0.8': + resolution: {integrity: sha512-ihSE9D3xQAupNg/aGDZaukqoUSXG2KfstWosVmFCG7jbMQPaj2ivxWtsB+CnYY/T4D6LX1GHKixwJLunNCffww==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.0.8': + resolution: {integrity: sha512-Io2prxFyN2jOCcu4qJbVoilo19caiD3kqkD3WR0q3yDA5HUCo83v4LrRtg55ZwniYACW64z36eV7gyVbOfORjA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.4': + resolution: {integrity: sha512-2MNFrDY8jkFYc9Il9DgLsHhMzuHnOYM1+CUYVWbzu9oT0hC7V7EcYvdCKeoll/Fcci04A+ERZ9wcc7cQ8lTkIA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1468,6 +1598,10 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1592,6 +1726,9 @@ packages: resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} engines: {node: '>=12'} + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -1600,6 +1737,10 @@ packages: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + close-with-grace@2.2.0: resolution: {integrity: sha512-OdcFxnxTm/AMLPHA4Aq3J1BLpkojXP7I4G5QBQLN5TT55ED/rk04rAoDbtfNnfZ988kGXPxh1bdRLeIU9bz/lA==} @@ -1893,6 +2034,10 @@ packages: resolution: {integrity: sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==} engines: {node: '>=4'} + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + fast-copy@3.0.2: resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} @@ -2126,6 +2271,10 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2136,6 +2285,15 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inquirer@12.4.1: + resolution: {integrity: sha512-/V7OyFkeUBFO2jAokUq5emSlcVMHVvzg8bwwZnzmCwErPgbeftsthmPUg71AIi5mR0YmiJOLQ+bTiHVWEjOw7A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + inspect-with-kind@1.0.5: resolution: {integrity: sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==} @@ -2441,6 +2599,10 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + nanoid@3.3.8: resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2468,6 +2630,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + p-cancelable@3.0.0: resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} engines: {node: '>=12.20'} @@ -2618,6 +2784,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-async@3.0.0: + resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} + engines: {node: '>=0.12.0'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2851,6 +3021,10 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2885,6 +3059,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + typescript@5.7.3: resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} engines: {node: '>=14.17'} @@ -2910,6 +3088,10 @@ packages: util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + uuid@11.0.5: + resolution: {integrity: sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==} + hasBin: true + uuidv7@1.0.2: resolution: {integrity: sha512-8JQkH4ooXnm1JCIhqTMbtmdnYEn6oKukBxHn1Ic9878jMkL7daTI7anTExfY18VRCX7tcdn5quzvCb6EWrR8PA==} hasBin: true @@ -3012,6 +3194,10 @@ packages: engines: {node: '>=8'} hasBin: true + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3051,6 +3237,10 @@ packages: resolution: {integrity: sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==} engines: {node: '>=12'} + yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} + zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} @@ -3533,6 +3723,122 @@ snapshots: dependencies: graphql: 16.10.0 + '@inquirer/checkbox@4.1.1(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.6(@types/node@22.13.1) + '@inquirer/figures': 1.0.10 + '@inquirer/type': 3.0.4(@types/node@22.13.1) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 22.13.1 + + '@inquirer/confirm@5.1.5(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.6(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + optionalDependencies: + '@types/node': 22.13.1 + + '@inquirer/core@10.1.6(@types/node@22.13.1)': + dependencies: + '@inquirer/figures': 1.0.10 + '@inquirer/type': 3.0.4(@types/node@22.13.1) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 22.13.1 + + '@inquirer/editor@4.2.6(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.6(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + external-editor: 3.1.0 + optionalDependencies: + '@types/node': 22.13.1 + + '@inquirer/expand@4.0.8(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.6(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 22.13.1 + + '@inquirer/figures@1.0.10': {} + + '@inquirer/input@4.1.5(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.6(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + optionalDependencies: + '@types/node': 22.13.1 + + '@inquirer/number@3.0.8(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.6(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + optionalDependencies: + '@types/node': 22.13.1 + + '@inquirer/password@4.0.8(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.6(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + ansi-escapes: 4.3.2 + optionalDependencies: + '@types/node': 22.13.1 + + '@inquirer/prompts@7.3.1(@types/node@22.13.1)': + dependencies: + '@inquirer/checkbox': 4.1.1(@types/node@22.13.1) + '@inquirer/confirm': 5.1.5(@types/node@22.13.1) + '@inquirer/editor': 4.2.6(@types/node@22.13.1) + '@inquirer/expand': 4.0.8(@types/node@22.13.1) + '@inquirer/input': 4.1.5(@types/node@22.13.1) + '@inquirer/number': 3.0.8(@types/node@22.13.1) + '@inquirer/password': 4.0.8(@types/node@22.13.1) + '@inquirer/rawlist': 4.0.8(@types/node@22.13.1) + '@inquirer/search': 3.0.8(@types/node@22.13.1) + '@inquirer/select': 4.0.8(@types/node@22.13.1) + optionalDependencies: + '@types/node': 22.13.1 + + '@inquirer/rawlist@4.0.8(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.6(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 22.13.1 + + '@inquirer/search@3.0.8(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.6(@types/node@22.13.1) + '@inquirer/figures': 1.0.10 + '@inquirer/type': 3.0.4(@types/node@22.13.1) + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 22.13.1 + + '@inquirer/select@4.0.8(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.6(@types/node@22.13.1) + '@inquirer/figures': 1.0.10 + '@inquirer/type': 3.0.4(@types/node@22.13.1) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 22.13.1 + + '@inquirer/type@3.0.4(@types/node@22.13.1)': + optionalDependencies: + '@types/node': 22.13.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -4032,6 +4338,10 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -4161,10 +4471,14 @@ snapshots: loupe: 3.1.3 pathval: 2.0.0 + chardet@0.7.0: {} + check-error@2.1.1: {} clean-stack@2.2.0: {} + cli-width@4.1.0: {} + close-with-grace@2.2.0: {} color-convert@2.0.1: @@ -4438,6 +4752,12 @@ snapshots: ext-list: 2.2.2 sort-keys-length: 1.0.1 + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + fast-copy@3.0.2: {} fast-decode-uri-component@1.0.1: {} @@ -4727,12 +5047,28 @@ snapshots: human-signals@2.1.0: {} + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} indent-string@4.0.0: {} inherits@2.0.4: {} + inquirer@12.4.1(@types/node@22.13.1): + dependencies: + '@inquirer/core': 10.1.6(@types/node@22.13.1) + '@inquirer/prompts': 7.3.1(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + ansi-escapes: 4.3.2 + mute-stream: 2.0.0 + run-async: 3.0.0 + rxjs: 7.8.1 + optionalDependencies: + '@types/node': 22.13.1 + inspect-with-kind@1.0.5: dependencies: kind-of: 6.0.3 @@ -5024,6 +5360,8 @@ snapshots: ms@2.1.3: {} + mute-stream@2.0.0: {} + nanoid@3.3.8: {} normalize-url@8.0.1: {} @@ -5044,6 +5382,8 @@ snapshots: dependencies: mimic-fn: 2.1.0 + os-tmpdir@1.0.2: {} + p-cancelable@3.0.0: {} p-map@4.0.0: @@ -5211,6 +5551,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.34.5 fsevents: 2.3.3 + run-async@3.0.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -5426,6 +5768,10 @@ snapshots: tinyspy@3.0.2: {} + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -5452,6 +5798,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-fest@0.21.3: {} + typescript@5.7.3: {} uint8array-extras@1.4.0: {} @@ -5477,6 +5825,8 @@ snapshots: is-typed-array: 1.1.15 which-typed-array: 1.1.18 + uuid@11.0.5: {} + uuidv7@1.0.2: {} vite-node@3.0.5(@types/node@22.13.1)(tsx@4.19.2): @@ -5583,6 +5933,12 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -5613,4 +5969,6 @@ snapshots: buffer-crc32: 0.2.13 pend: 1.2.0 + yoctocolors-cjs@2.1.2: {} + zod@3.24.1: {} diff --git a/scripts/dbManagement/sample_data/organization_memberships.json b/sample_data/organization_memberships.json similarity index 100% rename from scripts/dbManagement/sample_data/organization_memberships.json rename to sample_data/organization_memberships.json diff --git a/scripts/dbManagement/sample_data/organizations.json b/sample_data/organizations.json similarity index 100% rename from scripts/dbManagement/sample_data/organizations.json rename to sample_data/organizations.json diff --git a/scripts/dbManagement/sample_data/users.json b/sample_data/users.json similarity index 100% rename from scripts/dbManagement/sample_data/users.json rename to sample_data/users.json diff --git a/scripts/dbManagement/addSampleData.ts b/scripts/dbManagement/addSampleData.ts deleted file mode 100644 index 4492b08395a..00000000000 --- a/scripts/dbManagement/addSampleData.ts +++ /dev/null @@ -1,70 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { - disconnect, - ensureAdministratorExists, - insertCollections, - pingDB, -} from "./helpers"; - -type Collection = "users" | "organizations" | "organization_memberships"; - -export async function main(): Promise { - const collections: Collection[] = [ - "users", - "organizations", - "organization_memberships", - ]; - - try { - await pingDB(); - console.log("\n\x1b[32mSuccess:\x1b[0m Database connected successfully\n"); - } catch (error: unknown) { - throw new Error(`Database connection failed: ${error}`); - } - try { - await ensureAdministratorExists(); - console.log("\x1b[32mSuccess:\x1b[0m Administrator setup complete\n"); - } catch (error: unknown) { - console.error("\nError: Administrator creation failed", error); - throw new Error( - "\n\x1b[31mAdministrator access may be lost, try reimporting sample DB to restore access\x1b[0m\n", - ); - } - - try { - await insertCollections(collections); - console.log("\n\x1b[32mSuccess:\x1b[0m Sample Data added to the database"); - } catch (error: unknown) { - console.error("Error: ", error); - throw new Error("Error adding sample data"); - } - - return; -} - -const scriptPath = fileURLToPath(import.meta.url); -export const isMain = - process.argv[1] && path.resolve(process.argv[1]) === path.resolve(scriptPath); - -if (isMain) { - let exitCode = 0; - (async () => { - try { - await main(); - } catch (error: unknown) { - exitCode = 1; - } - try { - await disconnect(); - console.log( - "\n\x1b[32mSuccess:\x1b[0m Gracefully disconnecting from the database\n", - ); - } catch (error: unknown) { - console.error("Error: Cannot disconnect", error); - exitCode = 1; - } finally { - process.exit(exitCode); - } - })(); -} diff --git a/scripts/dbManagement/helpers.ts b/scripts/dbManagement/helpers.ts deleted file mode 100644 index ab8d27bcd7d..00000000000 --- a/scripts/dbManagement/helpers.ts +++ /dev/null @@ -1,421 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import readline from "node:readline"; -import { fileURLToPath } from "node:url"; -import { hash } from "@node-rs/argon2"; -import { sql } from "drizzle-orm"; -import type { AnyPgColumn, PgTable } from "drizzle-orm/pg-core"; -import { drizzle } from "drizzle-orm/postgres-js"; -import envSchema from "env-schema"; -import postgres from "postgres"; -import * as schema from "src/drizzle/schema"; -import { - type EnvConfig, - envConfigSchema, - envSchemaAjv, -} from "src/envConfigSchema"; -import { uuidv7 } from "uuidv7"; - -const envConfig = envSchema({ - ajv: envSchemaAjv, - dotenv: true, - schema: envConfigSchema, -}); - -// Get the directory name of the current module -export const dirname: string = path.dirname(fileURLToPath(import.meta.url)); - -// Create a new database client -export const queryClient = postgres({ - host: envConfig.API_POSTGRES_HOST, - port: Number(envConfig.API_POSTGRES_PORT) || 5432, - database: envConfig.API_POSTGRES_DATABASE || "", - username: envConfig.API_POSTGRES_USER || "", - password: envConfig.API_POSTGRES_PASSWORD || "", - ssl: envConfig.API_POSTGRES_SSL_MODE === "allow", -}); - -export const db = drizzle(queryClient, { schema }); - -/** - * Prompts the user for confirmation using the built-in readline module. - */ -export async function askUserToContinue(question: string): Promise { - return new Promise((resolve) => { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - rl.question(`${question} (y/n): `, (answer) => { - rl.close(); - resolve(answer.trim().toLowerCase() === "y"); - }); - }); -} - -/** - * Clears all tables in the database. - */ -export async function formatDatabase(): Promise { - type TableRow = { tablename: string }; - - try { - await db.transaction(async (tx) => { - const tables: TableRow[] = await tx.execute(sql` - SELECT tablename FROM pg_catalog.pg_tables - WHERE schemaname = 'public' - `); - const tableNames = tables.map((row) => sql.identifier(row.tablename)); - - if (tableNames.length > 0) { - await tx.execute( - sql`TRUNCATE TABLE ${sql.join(tableNames, sql`, `)} RESTART IDENTITY CASCADE;`, - ); - } - }); - - return true; - } catch (error) { - return false; - } -} - -export async function ensureAdministratorExists(): Promise { - const email = envConfig.API_ADMINISTRATOR_USER_EMAIL_ADDRESS; - - if (!email) { - throw new Error("API_ADMINISTRATOR_USER_EMAIL_ADDRESS is not defined."); - } - - const existingUser = await db.query.usersTable.findFirst({ - columns: { id: true, role: true }, - where: (fields, operators) => operators.eq(fields.emailAddress, email), - }); - - if (existingUser) { - if (existingUser.role !== "administrator") { - await db - .update(schema.usersTable) - .set({ role: "administrator" }) - .where(sql`email_address = ${email}`); - console.log("Role Change: Updated user role to administrator"); - } else { - console.log("\x1b[33mFound: Administrator user already exists\x1b[0m \n"); - } - return true; - } - - const userId = uuidv7(); - const password = envConfig.API_ADMINISTRATOR_USER_PASSWORD; - if (!password) { - throw new Error("API_ADMINISTRATOR_USER_PASSWORD is not defined."); - } - const passwordHash = await hash(password); - - await db.insert(schema.usersTable).values({ - id: userId, - emailAddress: email, - name: envConfig.API_ADMINISTRATOR_USER_NAME || "", - passwordHash, - role: "administrator", - isEmailAddressVerified: true, - creatorId: userId, - }); - - return true; -} - -/** - * Lists sample data files and their document counts in the sample_data directory. - */ -export async function listSampleData(): Promise { - try { - const sampleDataPath = path.resolve(dirname, "./sample_data"); - const files = await fs.readdir(sampleDataPath); - console.log(files); - console.log("Sample Data Files:\n"); - - console.log( - `${"| File Name".padEnd(30)}| Document Count | -${"|".padEnd(30, "-")}|----------------| -`, - ); - - for (const file of files) { - const filePath = path.resolve(sampleDataPath, file); - const stats = await fs.stat(filePath); - if (stats.isFile()) { - const data = await fs.readFile(filePath, "utf8"); - const docs = JSON.parse(data); - console.log( - `| ${file.padEnd(28)}| ${docs.length.toString().padEnd(15)}|`, - ); - } - } - console.log(); - } catch (err) { - throw new Error(`\x1b[31mError listing sample data: ${err}\x1b[0m`); - } - - return true; -} - -/** - * Check database connection - */ - -export async function pingDB(): Promise { - try { - await db.execute(sql`SELECT 1`); - } catch (error) { - throw new Error("Unable to connect to the database."); - } - return true; -} - -/** - * Check duplicate data - */ - -export async function checkAndInsertData( - table: PgTable, - rows: T[], - conflictTarget: AnyPgColumn | AnyPgColumn[], - batchSize: number, -): Promise { - if (!rows.length) return false; - - await db.transaction(async (tx) => { - for (let i = 0; i < rows.length; i += batchSize) { - const batch = rows.slice(i, i + batchSize); - await tx - .insert(table) - .values(batch) - .onConflictDoNothing({ - target: Array.isArray(conflictTarget) - ? conflictTarget - : [conflictTarget], - }); - } - }); - return true; -} - -/** - * Inserts data into specified tables. - * @param collections - Array of collection/table names to insert data into - * @param options - Options for loading data - */ - -export async function insertCollections( - collections: string[], -): Promise { - try { - await checkDataSize("Before"); - - const API_ADMINISTRATOR_USER_EMAIL_ADDRESS = - envConfig.API_ADMINISTRATOR_USER_EMAIL_ADDRESS; - if (!API_ADMINISTRATOR_USER_EMAIL_ADDRESS) { - throw new Error( - "\x1b[31mAPI_ADMINISTRATOR_USER_EMAIL_ADDRESS is not defined.\x1b[0m", - ); - } - - for (const collection of collections) { - const dataPath = path.resolve( - dirname, - `./sample_data/${collection}.json`, - ); - const fileContent = await fs.readFile(dataPath, "utf8"); - - switch (collection) { - case "users": { - const users = JSON.parse(fileContent).map( - (user: { - createdAt: string | number | Date; - updatedAt: string | number | Date; - }) => ({ - ...user, - createdAt: parseDate(user.createdAt), - updatedAt: parseDate(user.updatedAt), - }), - ) as (typeof schema.usersTable.$inferInsert)[]; - - await checkAndInsertData( - schema.usersTable, - users, - schema.usersTable.id, - 1000, - ); - - console.log( - "\n\x1b[35mAdded: Users table data (skipping duplicates)\x1b[0m", - ); - break; - } - - case "organizations": { - const organizations = JSON.parse(fileContent).map( - (org: { - createdAt: string | number | Date; - updatedAt: string | number | Date; - }) => ({ - ...org, - createdAt: parseDate(org.createdAt), - updatedAt: parseDate(org.updatedAt), - }), - ) as (typeof schema.organizationsTable.$inferInsert)[]; - - await checkAndInsertData( - schema.organizationsTable, - organizations, - schema.organizationsTable.id, - 1000, - ); - - const API_ADMINISTRATOR_USER = await db.query.usersTable.findFirst({ - columns: { - id: true, - }, - where: (fields, operators) => - operators.eq( - fields.emailAddress, - API_ADMINISTRATOR_USER_EMAIL_ADDRESS, - ), - }); - if (!API_ADMINISTRATOR_USER) { - throw new Error( - "\x1b[31mAPI_ADMINISTRATOR_USER_EMAIL_ADDRESS is not found in users table\x1b[0m", - ); - } - - const organizationAdminMembership = organizations.map((org) => ({ - organizationId: org.id, - memberId: API_ADMINISTRATOR_USER.id, - creatorId: API_ADMINISTRATOR_USER.id, - createdAt: new Date(), - role: "administrator", - })) as (typeof schema.organizationMembershipsTable.$inferInsert)[]; - - await checkAndInsertData( - schema.organizationMembershipsTable, - organizationAdminMembership, - [ - schema.organizationMembershipsTable.organizationId, - schema.organizationMembershipsTable.memberId, - ], - 1000, - ); - - console.log( - "\x1b[35mAdded: Organizations table data (skipping duplicates), plus admin memberships\x1b[0m", - ); - break; - } - - case "organization_memberships": { - const organizationMemberships = JSON.parse(fileContent).map( - (membership: { - createdAt: string | number | Date; - }) => ({ - ...membership, - createdAt: parseDate(membership.createdAt), - }), - ) as (typeof schema.organizationMembershipsTable.$inferInsert)[]; - - await checkAndInsertData( - schema.organizationMembershipsTable, - organizationMemberships, - [ - schema.organizationMembershipsTable.organizationId, - schema.organizationMembershipsTable.memberId, - ], - 1000, - ); - - console.log( - "\x1b[35mAdded: Organization_memberships data (skipping duplicates)\x1b[0m", - ); - break; - } - - default: - console.log(`\x1b[31mInvalid table name: ${collection}\x1b[0m`); - break; - } - } - - await checkDataSize("After"); - } catch (err) { - throw new Error(`\x1b[31mError adding data to tables: ${err}\x1b[0m`); - } - - return true; -} - -/** - * Parses a date string and returns a Date object. Returns null if the date is invalid. - * @param date - The date string to parse - * @returns The parsed Date object or null - */ -export function parseDate(date: string | number | Date): Date | null { - const parsedDate = new Date(date); - return Number.isNaN(parsedDate.getTime()) ? null : parsedDate; -} - -/** - * Checks record counts in specified tables after data insertion. - * @returns {Promise} - Returns true if data exists, false otherwise. - */ -export async function checkDataSize(stage: string): Promise { - try { - const tables = [ - { - name: "organization_memberships", - table: schema.organizationMembershipsTable, - }, - { name: "organizations", table: schema.organizationsTable }, - { name: "users", table: schema.usersTable }, - ]; - - console.log(`\nRecord Counts ${stage} Import:\n`); - - console.log( - `${"| Table Name".padEnd(30)}| Record Count | -${"|".padEnd(30, "-")}|----------------| -`, - ); - - let dataExists = false; - - for (const { name, table } of tables) { - const result = await db - .select({ count: sql`count(*)` }) - .from(table); - - const count = result?.[0]?.count ?? 0; - console.log(`| ${name.padEnd(28)}| ${count.toString().padEnd(15)}|`); - - if (count > 0) { - dataExists = true; - } - } - - return dataExists; - } catch (err) { - console.error(`\x1b[31mError checking record count: ${err}\x1b[0m`); - return false; - } -} - -export async function disconnect(): Promise { - try { - await queryClient.end(); - } catch (err) { - throw new Error( - `\x1b[31mError disconnecting from the database: ${err}\x1b[0m`, - ); - } - return true; -} diff --git a/scripts/dbManagement/resetDB.ts b/scripts/dbManagement/resetDB.ts deleted file mode 100644 index ad13ca27ece..00000000000 --- a/scripts/dbManagement/resetDB.ts +++ /dev/null @@ -1,76 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { - askUserToContinue, - disconnect, - ensureAdministratorExists, - formatDatabase, - pingDB, -} from "./helpers"; - -export async function main(): Promise { - const deleteExisting = await askUserToContinue( - "\x1b[31m Warning:\x1b[0m This will delete all data in the database. Are you sure you want to continue?", - ); - - if (deleteExisting) { - try { - await pingDB(); - console.log( - "\n\x1b[32mSuccess:\x1b[0m Database connected successfully\n", - ); - } catch (error: unknown) { - throw new Error(`Database connection failed: ${error}`); - } - try { - await formatDatabase(); - console.log("\n\x1b[32mSuccess:\x1b[0m Database formatted successfully"); - } catch (error: unknown) { - console.error( - "\n\x1b[31mError: Database formatting failed\n\x1b[0m", - error, - ); - console.error("\n\x1b[33mRolled back to previous state\x1b[0m"); - console.error("\n\x1b[33mPreserving administrator access\x1b[0m"); - } - try { - await ensureAdministratorExists(); - console.log("\x1b[32mSuccess:\x1b[0m Administrator access restored\n"); - } catch (error: unknown) { - console.error("\nError: Administrator creation failed", error); - console.error( - "\n\x1b[31mAdministrator access may be lost, try reformatting DB to restore access\x1b[0m\n", - ); - } - } else { - console.log("Operation cancelled"); - } - - return; -} - -const scriptPath = fileURLToPath(import.meta.url); -export const isMain = - process.argv[1] && path.resolve(process.argv[1]) === path.resolve(scriptPath); - -if (isMain) { - let exitCode = 0; - (async () => { - try { - await main(); - } catch (error: unknown) { - exitCode = 1; - } - try { - await disconnect(); - console.log( - "\n\x1b[32mSuccess:\x1b[0m Gracefully disconnecting from the database\n", - ); - } catch (error: unknown) { - console.error("Error: Cannot disconnect", error); - exitCode = 1; - } finally { - process.exit(exitCode); - } - })(); -} diff --git a/src/utilities/loadSampleData.ts b/src/utilities/loadSampleData.ts new file mode 100644 index 00000000000..aa51bddbfb1 --- /dev/null +++ b/src/utilities/loadSampleData.ts @@ -0,0 +1,305 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import dotenv from "dotenv"; +import { sql } from "drizzle-orm"; +import { drizzle } from "drizzle-orm/postgres-js"; +import inquirer from "inquirer"; +import postgres from "postgres"; +import * as schema from "../drizzle/schema"; + +dotenv.config(); + +const dirname: string = path.dirname(fileURLToPath(import.meta.url)); + +const queryClient = postgres({ + host: process.env.API_POSTGRES_HOST, + port: Number(process.env.API_POSTGRES_PORT), + database: process.env.API_POSTGRES_DATABASE, + username: process.env.API_POSTGRES_USER, + password: process.env.API_POSTGRES_PASSWORD, + ssl: process.env.API_POSTGRES_SSL_MODE === "true", +}); + +const db = drizzle(queryClient, { schema }); + +interface LoadOptions { + items?: string[]; + format?: boolean; +} + +/** + * Lists sample data files and their document counts in the sample_data directory. + */ +export async function listSampleData(): Promise { + try { + const sampleDataPath = path.resolve(dirname, "../../sample_data"); + const files = await fs.readdir(sampleDataPath); + + console.log("Sample Data Files:\n"); + + console.log( + `${"| File Name".padEnd(30)}| Document Count | +${"|".padEnd(30, "-")}|----------------| +`, + ); + + for (const file of files) { + const filePath = path.resolve(sampleDataPath, file); + const stats = await fs.stat(filePath); + if (stats.isFile()) { + const data = await fs.readFile(filePath, "utf8"); + const docs = JSON.parse(data); + console.log( + `| ${file.padEnd(28)}| ${docs.length.toString().padEnd(15)}|`, + ); + } + } + console.log(); + } catch (err) { + console.error("\x1b[31m", `Error listing sample data: ${err}`); + } +} + +/** + * Clears all tables in the database except for the specified user. + */ +async function formatDatabase(): Promise { + const emailToKeep = "administrator@email.com"; + + const tables = [ + schema.postsTable, + schema.organizationsTable, + schema.eventsTable, + schema.organizationMembershipsTable, + ]; + + for (const table of tables) { + await db.delete(table); + } + + // Delete all users except the specified one + await db + .delete(schema.usersTable) + .where(sql`email_address != ${emailToKeep}`); + + console.log("Cleared all tables except the specified user\n"); +} + +/** + * Inserts data into specified tables. + * @param collections - Array of collection/table names to insert data into + * @param options - Options for loading data + */ +async function insertCollections( + collections: string[], + options: LoadOptions = {}, +): Promise { + try { + if (options.format) { + await formatDatabase(); + } + + for (const collection of collections) { + const data = await fs.readFile( + path.resolve(dirname, `../../sample_data/${collection}.json`), + "utf8", + ); + + switch (collection) { + case "users": { + const users = JSON.parse(data).map( + (user: { + createdAt: string | number | Date; + updatedAt: string | number | Date; + }) => ({ + ...user, + createdAt: parseDate(user.createdAt), + updatedAt: parseDate(user.updatedAt), + }), + ) as (typeof schema.usersTable.$inferInsert)[]; + await db.insert(schema.usersTable).values(users); + break; + } + case "organizations": { + const organizations = JSON.parse(data).map( + (org: { + createdAt: string | number | Date; + updatedAt: string | number | Date; + }) => ({ + ...org, + createdAt: parseDate(org.createdAt), + updatedAt: parseDate(org.updatedAt), + }), + ) as (typeof schema.organizationsTable.$inferInsert)[]; + await db.insert(schema.organizationsTable).values(organizations); + + // Add API_ADMINISTRATOR_USER_EMAIL_ADDRESS as administrator of the all organization + const API_ADMINISTRATOR_USER_EMAIL_ADDRESS = + process.env.API_ADMINISTRATOR_USER_EMAIL_ADDRESS; + if (!API_ADMINISTRATOR_USER_EMAIL_ADDRESS) { + console.error( + "\x1b[31m", + "API_ADMINISTRATOR_USER_EMAIL_ADDRESS is not defined in .env file", + ); + return; + } + + const API_ADMINISTRATOR_USER = await db.query.usersTable.findFirst({ + columns: { + id: true, + }, + where: (fields, operators) => + operators.eq( + fields.emailAddress, + API_ADMINISTRATOR_USER_EMAIL_ADDRESS, + ), + }); + if (!API_ADMINISTRATOR_USER) { + console.error( + "\x1b[31m", + "API_ADMINISTRATOR_USER_EMAIL_ADDRESS is not found in users table", + ); + return; + } + + const organizationAdminMembership = organizations.map((org) => ({ + organizationId: org.id, + memberId: API_ADMINISTRATOR_USER.id, + creatorId: API_ADMINISTRATOR_USER.id, + createdAt: new Date(), + role: "administrator", + })) as (typeof schema.organizationMembershipsTable.$inferInsert)[]; + await db + .insert(schema.organizationMembershipsTable) + .values(organizationAdminMembership); + console.log( + "\x1b[35m", + "Added API_ADMINISTRATOR_USER as administrator of the all organization", + ); + break; + } + case "organization_memberships": { + // Add case for organization memberships + const organizationMemberships = JSON.parse(data).map( + (membership: { createdAt: string | number | Date }) => ({ + ...membership, + createdAt: parseDate(membership.createdAt), + }), + ) as (typeof schema.organizationMembershipsTable.$inferInsert)[]; + await db + .insert(schema.organizationMembershipsTable) + .values(organizationMemberships); + break; + } + + default: + console.log("\x1b[31m", `Invalid table name: ${collection}`); + break; + } + + console.log("\x1b[35m", `Added ${collection} table data`); + } + + await checkCountAfterImport(); + await queryClient.end(); + + console.log("\nTables populated successfully"); + } catch (err) { + console.error("\x1b[31m", `Error adding data to tables: ${err}`); + } finally { + process.exit(0); + } +} + +/** + * Parses a date string and returns a Date object. Returns null if the date is invalid. + * @param date - The date string to parse + * @returns The parsed Date object or null + */ +function parseDate(date: string | number | Date): Date | null { + const parsedDate = new Date(date); + return Number.isNaN(parsedDate.getTime()) ? null : parsedDate; +} + +/** + * Checks record counts in specified tables after data insertion. + * @returns {Promise} - Returns true if data exists, false otherwise. + */ +async function checkCountAfterImport(): Promise { + try { + const tables = [ + { name: "users", table: schema.usersTable }, + { name: "organizations", table: schema.organizationsTable }, + { + name: "organization_memberships", + table: schema.organizationMembershipsTable, + }, + ]; + + console.log("\nRecord Counts After Import:\n"); + + console.log( + `${"| Table Name".padEnd(30)}| Record Count | +${"|".padEnd(30, "-")}|----------------| +`, + ); + + let dataExists = false; + + for (const { name, table } of tables) { + const result = await db + .select({ count: sql`count(*)` }) + .from(table); + + const count = result?.[0]?.count ?? 0; + console.log(`| ${name.padEnd(28)}| ${count.toString().padEnd(15)}|`); + + if (count > 0) { + dataExists = true; + } + } + + return dataExists; + } catch (err) { + console.error("\x1b[31m", `Error checking record count: ${err}`); + return false; + } +} + +const collections = ["users", "organizations", "organization_memberships"]; // Add organization memberships to collections + +const args = process.argv.slice(2); +const options: LoadOptions = { + format: args.includes("--format") || args.includes("-f"), + items: undefined, +}; + +const itemsIndex = args.findIndex((arg) => arg === "--items" || arg === "-i"); +if (itemsIndex !== -1 && args[itemsIndex + 1]) { + const items = args[itemsIndex + 1]; + options.items = items ? items.split(",") : undefined; +} + +(async (): Promise => { + await listSampleData(); + + const existingData = await checkCountAfterImport(); + if (existingData) { + const { deleteExisting } = await inquirer.prompt([ + { + type: "confirm", + name: "deleteExisting", + message: + "Existing data found. Do you want to delete existing data and import the new data?", + default: false, + }, + ]); + + if (deleteExisting) { + options.format = true; + } + } + + await insertCollections(options.items || collections, options); +})(); diff --git a/test/scripts/dbManagement/addSampleData.test.ts b/test/scripts/dbManagement/addSampleData.test.ts deleted file mode 100644 index 4a2ca2cfc57..00000000000 --- a/test/scripts/dbManagement/addSampleData.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { main } from "scripts/dbManagement/addSampleData"; -import type { EnvConfig } from "src/envConfigSchema"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("env-schema", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...(actual as Record), - default: vi.fn( - (): Partial => ({ - API_POSTGRES_HOST: "postgres-test", - API_POSTGRES_PORT: 5432, - API_POSTGRES_PASSWORD: "password", - API_ADMINISTRATOR_USER_EMAIL_ADDRESS: "adminstrator@email.com", - API_ADMINISTRATOR_USER_PASSWORD: "password", - }), - ), - }; -}); - -import * as mainModule from "scripts/dbManagement/addSampleData"; -import * as helpers from "scripts/dbManagement/helpers"; - -describe("main function", () => { - beforeEach(() => { - vi.resetModules(); - }); - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("should connect to the database, ensure admin exists, insert collections", async () => { - vi.spyOn(helpers, "pingDB").mockResolvedValueOnce(true); - vi.spyOn(helpers, "ensureAdministratorExists").mockResolvedValueOnce(true); - vi.spyOn(helpers, "insertCollections").mockResolvedValueOnce(true); - - await main(); - - expect(helpers.pingDB).toHaveBeenCalled(); - expect(helpers.ensureAdministratorExists).toHaveBeenCalled(); - expect(helpers.insertCollections).toHaveBeenCalledWith([ - "users", - "organizations", - "organization_memberships", - ]); - }); - - it("should handle concurrent sample data insertion attempts", async () => { - vi.spyOn(helpers, "pingDB").mockResolvedValue(true); - vi.spyOn(helpers, "ensureAdministratorExists").mockResolvedValue(true); - vi.spyOn(helpers, "insertCollections").mockResolvedValue(true); - - // Create multiple concurrent insertion attempts - const attempts = Array(3) - .fill(null) - .map(() => main()); - - await expect(Promise.all(attempts)).resolves.not.toThrow(); - expect(helpers.insertCollections).toHaveBeenCalledTimes(3); - }); - - it("should throw an error if database connection fails", async () => { - vi.spyOn(helpers, "pingDB").mockRejectedValueOnce( - new Error("Connection failed"), - ); - vi.spyOn(helpers, "ensureAdministratorExists").mockResolvedValueOnce(true); - vi.spyOn(helpers, "insertCollections").mockResolvedValueOnce(true); - - const consoleErrorSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - - await expect(main()).rejects.toThrow("Database connection failed:"); - - expect(consoleErrorSpy).not.toHaveBeenCalled(); - expect(helpers.ensureAdministratorExists).not.toHaveBeenCalled(); - expect(helpers.insertCollections).not.toHaveBeenCalled(); - }); - - it("should log an error if ensuring admin fails", async () => { - vi.spyOn(helpers, "pingDB").mockResolvedValueOnce(true); - vi.spyOn(helpers, "ensureAdministratorExists").mockRejectedValueOnce( - new Error("Admin creation failed"), - ); - vi.spyOn(helpers, "insertCollections").mockResolvedValueOnce(true); - const consoleErrorSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - - await expect(main()).rejects.toThrow( - "\n\x1b[31mAdministrator access may be lost, try reimporting sample DB to restore access\x1b[0m\n", - ); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - "\nError: Administrator creation failed", - expect.any(Error), - ); - expect(helpers.insertCollections).not.toHaveBeenCalled(); - }); - - it("should log an error if inserting collections fails", async () => { - vi.spyOn(helpers, "pingDB").mockResolvedValueOnce(true); - vi.spyOn(helpers, "ensureAdministratorExists").mockResolvedValueOnce(true); - vi.spyOn(helpers, "insertCollections").mockRejectedValueOnce( - new Error("Insert collections failed"), - ); - const consoleErrorSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - - await expect(main()).rejects.toThrow("Error adding sample data"); - - expect(consoleErrorSpy).toHaveBeenCalledWith("Error: ", expect.any(Error)); - }); - - it("should not execute main() when imported", async () => { - const disconnectSpy = vi - .spyOn(helpers, "disconnect") - .mockResolvedValueOnce(true); - - await import("scripts/dbManagement/addSampleData"); - - await new Promise((resolve) => setTimeout(resolve, 2000)); - expect(mainModule.isMain).toBe(false); - expect(disconnectSpy).not.toHaveBeenCalled(); - }); -}); diff --git a/test/scripts/dbManagement/helpers.test.ts b/test/scripts/dbManagement/helpers.test.ts deleted file mode 100644 index b60445f3a75..00000000000 --- a/test/scripts/dbManagement/helpers.test.ts +++ /dev/null @@ -1,252 +0,0 @@ -import fs from "node:fs/promises"; -import readline from "node:readline"; -import mockMembership from "scripts/dbManagement/sample_data/organization_memberships.json"; -import mockOrganization from "scripts/dbManagement/sample_data/organizations.json"; -import mockUser from "scripts/dbManagement/sample_data/users.json"; -import type { EnvConfig } from "src/envConfigSchema"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("env-schema", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...(actual as Record), - default: vi.fn( - (): Partial => ({ - API_POSTGRES_HOST: "postgres-test", - API_POSTGRES_PORT: 5432, - API_POSTGRES_PASSWORD: "password", - API_ADMINISTRATOR_USER_EMAIL_ADDRESS: "adminstrator@email.com", - API_ADMINISTRATOR_USER_PASSWORD: "password", - }), - ), - }; -}); - -import * as helpers from "scripts/dbManagement/helpers"; - -describe("Database Mocking", () => { - beforeEach(async () => { - vi.restoreAllMocks(); - vi.resetModules(); - await helpers.ensureAdministratorExists(); - }); - afterEach(async () => { - vi.restoreAllMocks(); - await helpers.ensureAdministratorExists(); - }); - - /* - * Ask User to Continue function - * - */ - - it("should return true when user inputs 'y'", async () => { - const mockInterface = { - question: vi.fn().mockImplementation((_question, callback) => { - callback("y"); - }), - close: vi.fn(), - }; - - vi.spyOn(readline, "createInterface").mockReturnValue( - mockInterface as unknown as readline.Interface, - ); - - const result = await helpers.askUserToContinue("Do you want to continue?"); - expect(result).toBe(true); - expect(mockInterface.close).toHaveBeenCalled(); - }); - it("should return false when user inputs 'n'", async () => { - // Mock readline interface - const mockInterface = { - question: vi.fn().mockImplementation((_question, callback) => { - callback("n"); // Simulate user input 'n' - }), - close: vi.fn(), - }; - - vi.spyOn(readline, "createInterface").mockReturnValue( - mockInterface as unknown as readline.Interface, - ); - - const result = await helpers.askUserToContinue("Do you want to continue?"); - expect(result).toBe(false); - expect(mockInterface.close).toHaveBeenCalled(); - }); - - /* - * Parse Date function - * - */ - - it("should handle dates with different formats", () => { - expect(helpers.parseDate("2025/02/20")).toEqual(new Date("2025-02-20")); - }); - - it("should handle timezone edge cases", () => { - const date = new Date("2025-02-20T23:59:59.999Z"); - expect(helpers.parseDate(date.toISOString())).toEqual(date); - }); - - it("should correctly parse a valid timestamp", () => { - const timestamp = 1708387200000; // Example timestamp - expect(helpers.parseDate(timestamp)).toEqual(new Date(timestamp)); - }); - - it("should correctly parse a valid Date object", () => { - const date = new Date(); - expect(helpers.parseDate(date)).toEqual(date); - }); - - it("should return null for an invalid date string", () => { - expect(helpers.parseDate("invalid-date")).toBeNull(); - }); - - /* - * List Sample Data function - * - */ - - it("should list sample data", async () => { - const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - const response = await helpers.listSampleData(); - expect(response).toBe(true); - expect(consoleLogSpy).toHaveBeenCalledWith("Sample Data Files:\n"); - expect(consoleLogSpy).toHaveBeenCalledWith( - `| organization_memberships.json| ${mockMembership.length} |`, - ); - expect(consoleLogSpy).toHaveBeenCalledWith( - `| users.json | ${mockUser.length} |`, - ); - expect(consoleLogSpy).toHaveBeenCalledWith( - `| organizations.json | ${mockOrganization.length} |`, - ); - }); - - it("should handle an error while listing sample data", async () => { - vi.spyOn(fs, "readdir").mockRejectedValue( - new Error("Failed to read directory"), - ); - - await expect(helpers.listSampleData()).rejects.toThrow( - "Error listing sample data: Error: Failed to read directory", - ); - - vi.restoreAllMocks(); - }); - - /* - * Connect to DB - * - */ - - it("should return true when the database is reachable", async () => { - vi.spyOn(helpers, "pingDB").mockResolvedValue(true); - - const result = await helpers.pingDB(); - expect(result).toBe(true); - }); - - it("should throw an error when the database is not reachable", async () => { - vi.spyOn(helpers, "pingDB").mockRejectedValueOnce( - new Error("Connection failed"), - ); - - await expect(helpers.pingDB).rejects.toThrow("Connection failed"); - }); - - /* - * Ensuring Administrator function - * - */ - - it("should create an administrator user if none exists", async () => { - const format = await helpers.formatDatabase(); - const response = await helpers.ensureAdministratorExists(); - expect(response).toBe(true); - expect(format).toBe(true); - }); - - it("should skip if an administrator user exists", async () => { - await helpers.formatDatabase(); - const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - - await helpers.ensureAdministratorExists(); - - const response = await helpers.ensureAdministratorExists(); - - expect(consoleLogSpy).toHaveBeenCalledWith( - "\x1b[33mFound: Administrator user already exists\x1b[0m \n", - ); - expect(response).toBe(true); - }); - - /* - * List Database function - * - */ - - it("should return values from the database", async () => { - const collections = ["users", "organizations", "organization_memberships"]; - await helpers.formatDatabase(); - await helpers.ensureAdministratorExists(); - await helpers.insertCollections(collections); - - const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - - const response = await helpers.checkDataSize("Current"); - - expect(response).toBe(true); - expect(consoleLogSpy).toHaveBeenCalledWith( - "\nRecord Counts Current Import:\n", - ); - expect(consoleLogSpy).toHaveBeenCalledWith( - `| organization_memberships | ${ - mockMembership.length + mockOrganization.length - } |`, - ); - expect(consoleLogSpy).toHaveBeenCalledWith( - `| users | ${mockUser.length + 1} |`, - ); - expect(consoleLogSpy).toHaveBeenCalledWith( - `| organizations | ${mockOrganization.length} |`, - ); - }); - - /* - * Format Database function - * - */ - - it("should return 0 values from the database if format is success", async () => { - const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - const format = await helpers.formatDatabase(); - const response = await helpers.checkDataSize("Current"); - - expect(format).toBe(true); - expect(response).toBe(false); - expect(consoleLogSpy).toHaveBeenCalledWith( - "\nRecord Counts Current Import:\n", - ); - expect(consoleLogSpy).toHaveBeenCalledWith( - "| organization_memberships | 0 |", - ); - expect(consoleLogSpy).toHaveBeenCalledWith( - "| users | 0 |", - ); - expect(consoleLogSpy).toHaveBeenCalledWith( - "| organizations | 0 |", - ); - await helpers.ensureAdministratorExists(); - }); - - it("should throw an error if an issue occurs during database formatting", async () => { - vi.spyOn(helpers.db, "transaction").mockImplementation(async () => { - throw new Error("Restricted"); - }); - - await expect(helpers.formatDatabase()).resolves.toBe(false); - - vi.restoreAllMocks(); - }); -}); diff --git a/test/scripts/dbManagement/resetDB.test.ts b/test/scripts/dbManagement/resetDB.test.ts deleted file mode 100644 index 2965ab507b4..00000000000 --- a/test/scripts/dbManagement/resetDB.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { main } from "scripts/dbManagement/resetDB"; -import type { EnvConfig } from "src/envConfigSchema"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("env-schema", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...(actual as Record), - default: vi.fn( - (): Partial => ({ - API_POSTGRES_HOST: "postgres-test", - API_POSTGRES_PORT: 5432, - API_POSTGRES_PASSWORD: "password", - API_ADMINISTRATOR_USER_EMAIL_ADDRESS: "adminstrator@email.com", - API_ADMINISTRATOR_USER_PASSWORD: "password", - }), - ), - }; -}); - -import * as mainModule from "scripts/dbManagement/addSampleData"; -import * as helpers from "scripts/dbManagement/helpers"; -describe("main function", () => { - beforeEach(async () => { - vi.resetModules(); - await helpers.ensureAdministratorExists(); - }); - afterEach(async () => { - vi.restoreAllMocks(); - await helpers.ensureAdministratorExists(); - }); - - it("should confirm to format, format DB, restore administrator", async () => { - vi.spyOn(helpers, "askUserToContinue").mockResolvedValueOnce(true); - vi.spyOn(helpers, "pingDB").mockResolvedValueOnce(true); - vi.spyOn(helpers, "ensureAdministratorExists").mockResolvedValueOnce(true); - vi.spyOn(helpers, "formatDatabase").mockResolvedValueOnce(true); - - await main(); - - expect(helpers.pingDB).toHaveBeenCalled(); - expect(helpers.ensureAdministratorExists).toHaveBeenCalled(); - expect(helpers.formatDatabase).toHaveBeenCalled(); - }); - it("should abort when user declines to continue", async () => { - vi.spyOn(helpers, "askUserToContinue").mockResolvedValueOnce(false); - const formatSpy = vi.spyOn(helpers, "formatDatabase"); - const adminSpy = vi.spyOn(helpers, "ensureAdministratorExists"); - - await main(); - - expect(formatSpy).not.toHaveBeenCalled(); - expect(adminSpy).not.toHaveBeenCalled(); - }); - - it("should throw an error if database connection fails", async () => { - vi.spyOn(helpers, "askUserToContinue").mockResolvedValueOnce(true); - vi.spyOn(helpers, "pingDB").mockRejectedValueOnce( - new Error("Connection failed"), - ); - vi.spyOn(helpers, "ensureAdministratorExists").mockResolvedValueOnce(true); - vi.spyOn(helpers, "formatDatabase").mockResolvedValueOnce(true); - const consoleErrorSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - - await expect(main()).rejects.toThrow("Database connection failed:"); - - expect(consoleErrorSpy).not.toHaveBeenCalled(); - expect(helpers.ensureAdministratorExists).not.toHaveBeenCalled(); - expect(helpers.formatDatabase).not.toHaveBeenCalled(); - }); - - it("should log an error if formatting fails", async () => { - vi.spyOn(helpers, "askUserToContinue").mockResolvedValueOnce(true); - vi.spyOn(helpers, "pingDB").mockResolvedValueOnce(true); - vi.spyOn(helpers, "formatDatabase").mockRejectedValueOnce( - new Error("Format Failed"), - ); - vi.spyOn(helpers, "ensureAdministratorExists").mockResolvedValueOnce(true); - const consoleErrorSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - - await main(); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - "\n\x1b[31mError: Database formatting failed\n\x1b[0m", - expect.any(Error), - ); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - "\n\x1b[33mPreserving administrator access\x1b[0m", - ); - }); - - it("should log an error if ensuring admin fails", async () => { - vi.spyOn(helpers, "askUserToContinue").mockResolvedValueOnce(true); - vi.spyOn(helpers, "pingDB").mockResolvedValueOnce(true); - vi.spyOn(helpers, "formatDatabase").mockResolvedValueOnce(true); - vi.spyOn(helpers, "ensureAdministratorExists").mockRejectedValueOnce( - new Error("Admin creation failed"), - ); - const consoleErrorSpy = vi - .spyOn(console, "error") - .mockImplementation(() => {}); - - await main(); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - "\nError: Administrator creation failed", - expect.any(Error), - ); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - "\n\x1b[31mAdministrator access may be lost, try reformatting DB to restore access\x1b[0m\n", - ); - }); - - it("should not execute main() when imported", async () => { - const disconnectSpy = vi - .spyOn(helpers, "disconnect") - .mockResolvedValueOnce(true); - - await import("scripts/dbManagement/resetDB"); - - await new Promise((resolve) => setTimeout(resolve, 2000)); - expect(mainModule.isMain).toBe(false); - expect(disconnectSpy).not.toHaveBeenCalled(); - }); -}); diff --git a/vitest.config.ts b/vitest.config.ts index 316ed8df21b..ce06bef9b21 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ }, // https://vitest.dev/config/#fileparallelism // fileParallelism: true, - testTimeout: 15000, + // https://vitest.dev/config/#globalsetup globalSetup: ["./test/setup.ts"],