diff --git a/package.json b/package.json index c13053a490b..db0bc81d568 100644 --- a/package.json +++ b/package.json @@ -17,20 +17,17 @@ "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" }, @@ -40,7 +37,6 @@ "@faker-js/faker": "^9.4.0", "@swc/cli": "0.6.0", "@swc/core": "^1.10.9", - "@types/inquirer": "^9.0.7", "@types/node": "^22.10.7", "@types/uuid": "^10.0.0", "@vitest/coverage-v8": "^3.0.3", @@ -88,7 +84,8 @@ "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", - "import:sample-data": "tsx ./src/utilities/loadSampleData.ts", + "reset:data": "tsx ./scripts/dbManagement/resetData.ts", + "add:sample_data": "tsx ./scripts/dbManagement/addSampleData.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 808f8b18198..df290f8c4d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,9 +41,6 @@ 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) @@ -68,9 +65,6 @@ 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) @@ -86,9 +80,6 @@ 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 @@ -108,9 +99,6 @@ importers: '@swc/core': specifier: ^1.10.9 version: 1.10.14 - '@types/inquirer': - specifier: ^9.0.7 - version: 9.0.7 '@types/node': specifier: ^22.10.7 version: 22.13.1 @@ -908,127 +896,6 @@ 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'} @@ -1483,15 +1350,9 @@ packages: '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} - '@types/inquirer@9.0.7': - resolution: {integrity: sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==} - '@types/node@22.13.1': resolution: {integrity: sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==} - '@types/through@0.0.33': - resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} - '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} @@ -1598,10 +1459,6 @@ 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'} @@ -1726,9 +1583,6 @@ 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'} @@ -1737,10 +1591,6 @@ 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==} @@ -2034,10 +1884,6 @@ 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==} @@ -2271,10 +2117,6 @@ 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==} @@ -2285,15 +2127,6 @@ 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==} @@ -2599,10 +2432,6 @@ 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} @@ -2630,10 +2459,6 @@ 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'} @@ -2784,16 +2609,9 @@ 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==} - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -3021,10 +2839,6 @@ 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'} @@ -3059,10 +2873,6 @@ 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'} @@ -3088,10 +2898,6 @@ 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 @@ -3194,10 +3000,6 @@ 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'} @@ -3237,10 +3039,6 @@ 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==} @@ -3723,122 +3521,6 @@ 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 @@ -4172,19 +3854,10 @@ snapshots: '@types/http-cache-semantics@4.0.4': {} - '@types/inquirer@9.0.7': - dependencies: - '@types/through': 0.0.33 - rxjs: 7.8.1 - '@types/node@22.13.1': dependencies: undici-types: 6.20.0 - '@types/through@0.0.33': - dependencies: - '@types/node': 22.13.1 - '@types/uuid@10.0.0': {} '@vitest/coverage-v8@3.0.5(vitest@3.0.5(@types/node@22.13.1)(tsx@4.19.2))': @@ -4338,10 +4011,6 @@ 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: {} @@ -4471,14 +4140,10 @@ 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: @@ -4752,12 +4417,6 @@ 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: {} @@ -5047,28 +4706,12 @@ 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 @@ -5360,8 +5003,6 @@ snapshots: ms@2.1.3: {} - mute-stream@2.0.0: {} - nanoid@3.3.8: {} normalize-url@8.0.1: {} @@ -5382,8 +5023,6 @@ snapshots: dependencies: mimic-fn: 2.1.0 - os-tmpdir@1.0.2: {} - p-cancelable@3.0.0: {} p-map@4.0.0: @@ -5551,16 +5190,10 @@ 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 - rxjs@7.8.1: - dependencies: - tslib: 2.8.1 - safe-buffer@5.2.1: {} safe-regex-test@1.1.0: @@ -5768,10 +5401,6 @@ 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 @@ -5798,8 +5427,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - type-fest@0.21.3: {} - typescript@5.7.3: {} uint8array-extras@1.4.0: {} @@ -5825,8 +5452,6 @@ 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): @@ -5933,12 +5558,6 @@ 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 @@ -5969,6 +5588,4 @@ snapshots: buffer-crc32: 0.2.13 pend: 1.2.0 - yoctocolors-cjs@2.1.2: {} - zod@3.24.1: {} diff --git a/sample_data/images/01JMQ3EABXAD1F3KTW1NQGF99C.jpeg b/sample_data/images/01JMQ3EABXAD1F3KTW1NQGF99C.jpeg deleted file mode 100644 index 97107dedc30..00000000000 Binary files a/sample_data/images/01JMQ3EABXAD1F3KTW1NQGF99C.jpeg and /dev/null differ diff --git a/sample_data/images/01JMQ3ETE4Q257BJV59TBSNJMN.webp b/sample_data/images/01JMQ3ETE4Q257BJV59TBSNJMN.webp deleted file mode 100644 index 5eb08519cde..00000000000 Binary files a/sample_data/images/01JMQ3ETE4Q257BJV59TBSNJMN.webp and /dev/null differ diff --git a/sample_data/images/01JMQ3F1B55K272B25V19781D0.jpeg b/sample_data/images/01JMQ3F1B55K272B25V19781D0.jpeg deleted file mode 100644 index 272edc0f51f..00000000000 Binary files a/sample_data/images/01JMQ3F1B55K272B25V19781D0.jpeg and /dev/null differ diff --git a/sample_data/images/01JMQ3F9E5C806CN6M8EV31E01.jpeg b/sample_data/images/01JMQ3F9E5C806CN6M8EV31E01.jpeg deleted file mode 100644 index 97107dedc30..00000000000 Binary files a/sample_data/images/01JMQ3F9E5C806CN6M8EV31E01.jpeg and /dev/null differ diff --git a/sample_data/images/01JMQ3FGBEDRB9AM6XP9C97RF8.jpeg b/sample_data/images/01JMQ3FGBEDRB9AM6XP9C97RF8.jpeg deleted file mode 100644 index f29549b7613..00000000000 Binary files a/sample_data/images/01JMQ3FGBEDRB9AM6XP9C97RF8.jpeg and /dev/null differ diff --git a/sample_data/images/01JMQ3FPS789Z1J6BBQ01YDHVQ.jpeg b/sample_data/images/01JMQ3FPS789Z1J6BBQ01YDHVQ.jpeg deleted file mode 100644 index 5eaf2b3a8b4..00000000000 Binary files a/sample_data/images/01JMQ3FPS789Z1J6BBQ01YDHVQ.jpeg and /dev/null differ diff --git a/scripts/dbManagement/addSampleData.ts b/scripts/dbManagement/addSampleData.ts new file mode 100644 index 00000000000..6389ac7ca97 --- /dev/null +++ b/scripts/dbManagement/addSampleData.ts @@ -0,0 +1,83 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + disconnect, + ensureAdministratorExists, + insertCollections, + pingDB, +} from "./helpers"; + +type Collection = + | "users" + | "organizations" + | "organization_memberships" + | "posts" + | "post_votes" + | "post_attachments" + | "comments" + | "comment_votes"; + +export async function main(): Promise { + const collections: Collection[] = [ + "users", + "organizations", + "organization_memberships", + "posts", + "post_votes", + "post_attachments", + "comments", + "comment_votes", + ]; + + 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 new file mode 100644 index 00000000000..83fdd26605e --- /dev/null +++ b/scripts/dbManagement/helpers.ts @@ -0,0 +1,595 @@ +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 { Client as MinioClient } from "minio"; +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)); +export const bucketName: string = envConfig.MINIO_ROOT_USER || ""; +// 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", +}); + +//Create a bucket client +const minioClient = new MinioClient({ + accessKey: envConfig.API_MINIO_ACCESS_KEY || "", + endPoint: envConfig.API_MINIO_END_POINT || "", + port: Number(envConfig.API_MINIO_PORT), + secretKey: envConfig.API_MINIO_SECRET_KEY || "", + useSSL: envConfig.API_MINIO_USE_SSL === true, +}); + +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( + "\x1b[33mRole Change: Updated user role to administrator\x1b[0m\n", + ); + } else { + console.log("\x1b[32mFound:\x1b[0m Administrator user already exists"); + } + 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; +} + +export async function emptyMinioBucket(): Promise { + try { + // List all objects in the bucket. + const objectsList: string[] = await new Promise( + (resolve, reject) => { + const objects: string[] = []; + const stream = minioClient.listObjects(bucketName, "", true); + stream.on( + "data", + (obj: { + name: string; + }) => { + objects.push(obj.name); + }, + ); + stream.on("error", (err: Error) => { + console.error("Error listing objects in bucket:", err); + reject(err); + }); + stream.on("end", () => resolve(objects)); + }, + ); + + // If there are objects, remove them all using removeObjects. + if (objectsList.length > 0) { + await minioClient.removeObjects(bucketName, objectsList); + } + + return true; + } catch (error: unknown) { + console.error("Error emptying bucket:", error); + return false; + } +} + +/** + * 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; + } + case "posts": { + const posts = JSON.parse(fileContent).map( + (post: { createdAt: string | number | Date }) => ({ + ...post, + createdAt: parseDate(post.createdAt), + }), + ) as (typeof schema.postsTable.$inferInsert)[]; + await checkAndInsertData( + schema.postsTable, + posts, + schema.postsTable.id, + 1000, + ); + console.log( + "\x1b[35mAdded: Posts table data (skipping duplicates)\x1b[0m", + ); + break; + } + case "post_votes": { + const post_votes = JSON.parse(fileContent).map( + (post_vote: { createdAt: string | number | Date }) => ({ + ...post_vote, + createdAt: parseDate(post_vote.createdAt), + }), + ) as (typeof schema.postVotesTable.$inferInsert)[]; + await checkAndInsertData( + schema.postVotesTable, + post_votes, + schema.postVotesTable.id, + 1000, + ); + console.log( + "\x1b[35mAdded: Post_votes table data (skipping duplicates)\x1b[0m", + ); + break; + } + case "post_attachments": { + const post_attachments = JSON.parse(fileContent).map( + (post_attachment: { createdAt: string | number | Date }) => ({ + ...post_attachment, + createdAt: parseDate(post_attachment.createdAt), + }), + ) as (typeof schema.postAttachmentsTable.$inferInsert)[]; + try { + // Post Attachements are not unique. So they are inserted without checking for duplicates. + await db + .insert(schema.postAttachmentsTable) + .values(post_attachments); + } catch { + throw new Error( + "\x1b[31mError inserting post_attachments data\x1b[0m", + ); + } + // Handle file uploads to Minio. + await Promise.all( + post_attachments.map(async (attachment) => { + try { + const fileExtension = attachment.mimeType.split("/").pop(); + const filePath = path.resolve( + dirname, + `./sample_data/images/${attachment.name}.${fileExtension}`, + ); + const fileData = await fs.readFile(filePath); + await minioClient.putObject( + bucketName, + attachment.name, + fileData, + undefined, + { + "content-type": attachment.mimeType, + }, + ); + } catch (error) { + console.error( + `Failed to upload attachment ${attachment.name}:`, + error, + ); + throw error; + } + }), + ); + console.log( + "\x1b[35mAdded: Post_attachments table data and uploaded files (Duplicates Allowed)\x1b[0m", + ); + break; + } + case "comments": { + const comments = JSON.parse(fileContent).map( + (comment: { createdAt: string | number | Date }) => ({ + ...comment, + createdAt: parseDate(comment.createdAt), + }), + ) as (typeof schema.commentsTable.$inferInsert)[]; + await checkAndInsertData( + schema.commentsTable, + comments, + schema.commentsTable.id, + 1000, + ); + console.log( + "\x1b[35mAdded: Comments table data (skipping duplicates)\x1b[0m", + ); + break; + } + case "comment_votes": { + const comment_votes = JSON.parse(fileContent).map( + (comment_vote: { createdAt: string | number | Date }) => ({ + ...comment_vote, + createdAt: parseDate(comment_vote.createdAt), + }), + ) as (typeof schema.commentVotesTable.$inferInsert)[]; + await checkAndInsertData( + schema.commentVotesTable, + comment_votes, + schema.commentVotesTable.id, + 1000, + ); + console.log( + "\x1b[35mAdded: Comment_votes table 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: "users", table: schema.usersTable }, + { name: "organizations", table: schema.organizationsTable }, + { + name: "organization_memberships", + table: schema.organizationMembershipsTable, + }, + { name: "posts", table: schema.postsTable }, + { name: "post_votes", table: schema.postVotesTable }, + { name: "post_attachments", table: schema.postAttachmentsTable }, + { name: "comments", table: schema.commentsTable }, + { name: "comment_votes", table: schema.commentVotesTable }, + ]; + + 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/resetData.ts b/scripts/dbManagement/resetData.ts new file mode 100644 index 00000000000..5cba46be687 --- /dev/null +++ b/scripts/dbManagement/resetData.ts @@ -0,0 +1,86 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + askUserToContinue, + disconnect, + emptyMinioBucket, + 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 emptyMinioBucket(); + console.log("\x1b[32mSuccess:\x1b[0m Bucket formatted successfully\n"); + } catch (error: unknown) { + console.error( + "\n\x1b[31mError: Bucket formatting failed\n\x1b[0m", + error, + ); + } + 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/sample_data/comment_votes.json b/scripts/dbManagement/sample_data/comment_votes.json similarity index 100% rename from sample_data/comment_votes.json rename to scripts/dbManagement/sample_data/comment_votes.json diff --git a/sample_data/comments.json b/scripts/dbManagement/sample_data/comments.json similarity index 100% rename from sample_data/comments.json rename to scripts/dbManagement/sample_data/comments.json diff --git "a/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_1.webp" "b/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_1.webp" new file mode 100644 index 00000000000..f65dadae1d3 Binary files /dev/null and "b/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_1.webp" differ diff --git "a/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_2.webp" "b/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_2.webp" new file mode 100644 index 00000000000..b1ce5e2f72f Binary files /dev/null and "b/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_2.webp" differ diff --git "a/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_3.webp" "b/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_3.webp" new file mode 100644 index 00000000000..8f9148e68ab Binary files /dev/null and "b/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_3.webp" differ diff --git "a/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_4.webp" "b/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_4.webp" new file mode 100644 index 00000000000..f65dadae1d3 Binary files /dev/null and "b/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_4.webp" differ diff --git "a/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_5.webp" "b/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_5.webp" new file mode 100644 index 00000000000..b1ce5e2f72f Binary files /dev/null and "b/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_5.webp" differ diff --git "a/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_6.webp" "b/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_6.webp" new file mode 100644 index 00000000000..8f9148e68ab Binary files /dev/null and "b/scripts/dbManagement/sample_data/images/DALL\302\267E_talawa_6.webp" differ diff --git a/sample_data/organization_memberships.json b/scripts/dbManagement/sample_data/organization_memberships.json similarity index 100% rename from sample_data/organization_memberships.json rename to scripts/dbManagement/sample_data/organization_memberships.json diff --git a/sample_data/organizations.json b/scripts/dbManagement/sample_data/organizations.json similarity index 100% rename from sample_data/organizations.json rename to scripts/dbManagement/sample_data/organizations.json diff --git a/sample_data/post_attachments.json b/scripts/dbManagement/sample_data/post_attachments.json similarity index 77% rename from sample_data/post_attachments.json rename to scripts/dbManagement/sample_data/post_attachments.json index fc3a1d198bc..e5867633888 100644 --- a/sample_data/post_attachments.json +++ b/scripts/dbManagement/sample_data/post_attachments.json @@ -4,8 +4,8 @@ "createdAt": "2025-02-10T12:10:00.000Z", "creatorId": "0194e194-c6b3-7802-b074-362efea24dbc", "postId": "2778f4a2-a6f1-45cc-857f-727ca942899d", - "mimeType": "image/jpeg", - "name": "01JMQ3EABXAD1F3KTW1NQGF99C" + "mimeType": "image/webp", + "name": "DALL·E_talawa_1" }, { "id": "e1bba4bb-5edd-40fd-9baf-741647d26ec0", @@ -13,39 +13,39 @@ "creatorId": "0194e194-c6b3-7802-b074-362efea24dbc", "postId": "98cef234-af91-4a11-b33f-5fd6cf41219c", "mimeType": "image/webp", - "name": "01JMQ3ETE4Q257BJV59TBSNJMN" + "name": "DALL·E_talawa_2" }, { "id": "5065ab3d-51bd-4c6e-b3e3-389af5610cc3", "createdAt": "2025-02-10T12:20:00.000Z", "creatorId": "65378abd-8500-8f17-1cf2-990d00000002", "postId": "a44d75a0-267f-4429-b03c-a26cc4ec56dc", - "mimeType": "image/jpeg", - "name": "01JMQ3F1B55K272B25V19781D0" + "mimeType": "image/webp", + "name": "DALL·E_talawa_3" }, { "id": "ea15240d-269d-4c94-8f7c-fa4723bd3413", "createdAt": "2025-02-10T12:21:00.000Z", "creatorId": "65378abd-8500-8f17-1cf2-990d00000002", "postId": "f3da248b-ee18-4197-bd0b-de5480c010ee", - "mimeType": "image/jpeg", - "name": "01JMQ3F9E5C806CN6M8EV31E01" + "mimeType": "image/webp", + "name": "DALL·E_talawa_4" }, { "id": "c898e282-29e4-4f0c-abb3-0829c6091575", "createdAt": "2025-02-10T12:30:00.000Z", "creatorId": "67378abd-8500-8f17-1cf2-990d00000005", "postId": "885bee36-2751-4d78-bfd4-7ededdd89a0f", - "mimeType": "image/jpeg", - "name": "01JMQ3FGBEDRB9AM6XP9C97RF8" + "mimeType": "image/webp", + "name": "DALL·E_talawa_5" }, { "id": "685b4d55-9627-474d-a8d8-d36aaf34a93d", "createdAt": "2025-02-10T12:31:00.000Z", "creatorId": "67378abd-8500-8f17-1cf2-990d00000005", "postId": "ecaeed44-d0b2-4559-bad6-ab6f49362325", - "mimeType": "image/jpeg", - "name": "01JMQ3FPS789Z1J6BBQ01YDHVQ" + "mimeType": "image/webp", + "name": "DALL·E_talawa_6" } ] \ No newline at end of file diff --git a/sample_data/post_votes.json b/scripts/dbManagement/sample_data/post_votes.json similarity index 100% rename from sample_data/post_votes.json rename to scripts/dbManagement/sample_data/post_votes.json diff --git a/sample_data/posts.json b/scripts/dbManagement/sample_data/posts.json similarity index 100% rename from sample_data/posts.json rename to scripts/dbManagement/sample_data/posts.json diff --git a/sample_data/users.json b/scripts/dbManagement/sample_data/users.json similarity index 100% rename from sample_data/users.json rename to scripts/dbManagement/sample_data/users.json diff --git a/src/envConfigSchema.ts b/src/envConfigSchema.ts index ed0dde8c057..a2a825651f8 100644 --- a/src/envConfigSchema.ts +++ b/src/envConfigSchema.ts @@ -161,6 +161,10 @@ export const envConfigSchema = Type.Object({ trace: "trace", warn: "warn", }), + /** + * More information can be found at: {@link https://github.com/minio/minio-js?tab=readme-ov-file#initialize-minio-client} + */ + MINIO_ROOT_USER: Type.Optional(Type.String({ minLength: 1 })), /** * More information can be found at: {@link https://github.com/minio/minio-js?tab=readme-ov-file#initialize-minio-client} */ diff --git a/src/utilities/loadSampleData.ts b/src/utilities/loadSampleData.ts deleted file mode 100644 index 57664582441..00000000000 --- a/src/utilities/loadSampleData.ts +++ /dev/null @@ -1,405 +0,0 @@ -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 { Client as MinioClient } from "minio"; -import postgres from "postgres"; -import * as schema from "../drizzle/schema"; - -dotenv.config(); - -const dirname: string = path.dirname(fileURLToPath(import.meta.url)); - -const minioClient = new MinioClient({ - accessKey: process.env.API_MINIO_ACCESS_KEY || "", - endPoint: process.env.API_MINIO_END_POINT || "minio", - port: Number(process.env.API_MINIO_PORT), - secretKey: process.env.API_MINIO_SECRET_KEY || "", - useSSL: process.env.API_MINIO_USE_SSL === "true", -}); - -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; - } - case "posts": { - const posts = JSON.parse(data).map( - (post: { createdAt: string | number | Date }) => ({ - ...post, - createdAt: parseDate(post.createdAt), - }), - ) as (typeof schema.postsTable.$inferInsert)[]; - await db.insert(schema.postsTable).values(posts); - break; - } - case "post_votes": { - const post_votes = JSON.parse(data).map( - (post_vote: { createdAt: string | number | Date }) => ({ - ...post_vote, - createdAt: parseDate(post_vote.createdAt), - }), - ) as (typeof schema.postVotesTable.$inferInsert)[]; - await db.insert(schema.postVotesTable).values(post_votes); - break; - } - case "post_attachments": { - const post_attachments = JSON.parse(data).map( - (post_attachment: { createdAt: string | number | Date }) => ({ - ...post_attachment, - createdAt: parseDate(post_attachment.createdAt), - }), - ) as (typeof schema.postAttachmentsTable.$inferInsert)[]; - await db.insert(schema.postAttachmentsTable).values(post_attachments); - await Promise.all( - post_attachments.map(async (attachment) => { - try { - const fileExtension = attachment.mimeType.split("/").pop(); - const filePath = path.resolve( - dirname, - `../../sample_data/images/${attachment.name}.${fileExtension}`, - ); - const readStream = await fs.readFile(filePath); - await minioClient.putObject( - "talawa", - attachment.name, - readStream, - undefined, - { - "content-type": attachment.mimeType, - }, - ); - } catch (error) { - console.error( - `Failed to upload attachment ${attachment.name}:`, - error, - ); - throw error; - } - }), - ); - break; - } - case "comments": { - const comments = JSON.parse(data).map( - (comment: { createdAt: string | number | Date }) => ({ - ...comment, - createdAt: parseDate(comment.createdAt), - }), - ) as (typeof schema.commentsTable.$inferInsert)[]; - await db.insert(schema.commentsTable).values(comments); - break; - } - case "comment_votes": { - const comment_votes = JSON.parse(data).map( - (comment_vote: { createdAt: string | number | Date }) => ({ - ...comment_vote, - createdAt: parseDate(comment_vote.createdAt), - }), - ) as (typeof schema.commentVotesTable.$inferInsert)[]; - await db.insert(schema.commentVotesTable).values(comment_votes); - 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, - }, - { name: "posts", table: schema.postsTable }, - { name: "post_votes", table: schema.postVotesTable }, - { name: "post_attachments", table: schema.postAttachmentsTable }, - { name: "comments", table: schema.commentsTable }, - { name: "comment_votes", table: schema.commentVotesTable }, - ]; - - 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", - "posts", - "post_votes", - "post_attachments", - "comments", - "comment_votes", -]; // Add posts, votes, attachments, comments, and membership data 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 new file mode 100644 index 00000000000..bfbf2521bc3 --- /dev/null +++ b/test/scripts/dbManagement/addSampleData.test.ts @@ -0,0 +1,89 @@ +import * as mainModule from "scripts/dbManagement/addSampleData"; +import * as helpers from "scripts/dbManagement/helpers"; +import { beforeEach, expect, suite, test, vi } from "vitest"; + +suite("addSampleData main function tests", () => { + beforeEach(async () => { + vi.resetModules(); + }); + + test("should execute all operations successfully", async () => { + // Arrange: simulate successful operations. + const pingDBSpy = vi.spyOn(helpers, "pingDB").mockResolvedValue(true); + const ensureAdminSpy = vi + .spyOn(helpers, "ensureAdministratorExists") + .mockResolvedValue(true); + const insertCollectionsSpy = vi + .spyOn(helpers, "insertCollections") + .mockResolvedValue(true); + + // Spy on console.log to verify the output messages. + const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // Act: call main() + await expect(mainModule.main()).resolves.toBeUndefined(); + + // Assert: verify that each helper was called as expected. + expect(pingDBSpy).toHaveBeenCalled(); + expect(ensureAdminSpy).toHaveBeenCalled(); + expect(insertCollectionsSpy).toHaveBeenCalledWith([ + "users", + "organizations", + "organization_memberships", + "posts", + "post_votes", + "post_attachments", + "comments", + "comment_votes", + ]); + + // Verify that success messages are logged. + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining("Database connected successfully"), + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining("Administrator setup complete"), + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining("Sample Data added to the database"), + ); + }); + + test("should throw an error when pingDB fails", async () => { + // Arrange: simulate failure of pingDB. + const errorMsg = "pingDB error"; + vi.spyOn(helpers, "pingDB").mockRejectedValue(new Error(errorMsg)); + + // Act & Assert: main() should throw an error indicating DB connection failure. + await expect(mainModule.main()).rejects.toThrow( + `Database connection failed: Error: ${errorMsg}`, + ); + }); + + test("should throw an error when ensureAdministratorExists fails", async () => { + // Arrange: pingDB succeeds, but ensureAdministratorExists fails. + vi.spyOn(helpers, "pingDB").mockResolvedValue(true); + const errorMsg = "admin error"; + vi.spyOn(helpers, "ensureAdministratorExists").mockRejectedValue( + new Error(errorMsg), + ); + + // Act & Assert: main() should throw the specific error message. + await expect(mainModule.main()).rejects.toThrow( + "\n\x1b[31mAdministrator access may be lost, try reimporting sample DB to restore access\x1b[0m\n", + ); + }); + + test("should throw an error when insertCollections fails", async () => { + // Arrange: pingDB and ensureAdministratorExists succeed, but insertCollections fails. + vi.spyOn(helpers, "pingDB").mockResolvedValue(true); + vi.spyOn(helpers, "ensureAdministratorExists").mockResolvedValue(true); + const errorMsg = "insert error"; + vi.spyOn(helpers, "insertCollections").mockRejectedValue( + new Error(errorMsg), + ); + + // Act & Assert: main() should throw an error indicating sample data insertion failure. + await expect(mainModule.main()).rejects.toThrow("Error adding sample data"); + }); +}); diff --git a/test/scripts/dbManagement/helpers.test.ts b/test/scripts/dbManagement/helpers.test.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/scripts/dbManagement/resetDB.test.ts b/test/scripts/dbManagement/resetDB.test.ts new file mode 100644 index 00000000000..4c4ca99e09a --- /dev/null +++ b/test/scripts/dbManagement/resetDB.test.ts @@ -0,0 +1,116 @@ +import * as helpers from "scripts/dbManagement/helpers"; +import * as resetDataModule from "scripts/dbManagement/resetData"; +// tests/dbManagement/resetData.test.ts +import { beforeEach, expect, suite, test, vi } from "vitest"; + +suite("resetData main function tests", () => { + beforeEach(() => { + // Reset all mocks before each test. + vi.resetAllMocks(); + }); + + test("should cancel operation if askUserToContinue returns false", async () => { + // Arrange: simulate the user declining the operation. + vi.spyOn(helpers, "askUserToContinue").mockResolvedValue(false); + const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // Act: call main() + await resetDataModule.main(); + + // Assert: verify that "Operation cancelled" is logged. + expect(consoleLogSpy).toHaveBeenCalledWith("Operation cancelled"); + }); + + test("should throw error when pingDB fails", async () => { + // Arrange: simulate user confirming and pingDB failure. + vi.spyOn(helpers, "askUserToContinue").mockResolvedValue(true); + vi.spyOn(helpers, "pingDB").mockRejectedValue(new Error("ping error")); + + // Act & Assert: main() should throw an error indicating DB connection failure. + await expect(resetDataModule.main()).rejects.toThrow( + "Database connection failed: Error: ping error", + ); + }); + + test("should log errors for failing formatDatabase, emptyMinioBucket, and ensureAdministratorExists, but not throw", async () => { + // Arrange: simulate user confirming and pingDB succeeding. + vi.spyOn(helpers, "askUserToContinue").mockResolvedValue(true); + vi.spyOn(helpers, "pingDB").mockResolvedValue(true); + // Simulate failures for subsequent operations. + vi.spyOn(helpers, "formatDatabase").mockRejectedValue( + new Error("format error"), + ); + vi.spyOn(helpers, "emptyMinioBucket").mockRejectedValue( + new Error("minio error"), + ); + vi.spyOn(helpers, "ensureAdministratorExists").mockRejectedValue( + new Error("admin error"), + ); + + const consoleErrorSpy = vi + .spyOn(console, "error") + .mockImplementation(() => {}); + const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // Act: call main(). In this case main() should resolve (it doesn't throw after pingDB), + // and the errors should be logged. + await expect(resetDataModule.main()).resolves.toBeUndefined(); + + // Assert: verify that pingDB success was logged. + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining("Database connected successfully"), + ); + // Verify errors were logged for formatting, bucket cleanup, and admin creation. + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("Error: Database formatting failed"), + expect.any(Error), + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("Rolled back to previous state"), + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("Preserving administrator access"), + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("Error: Bucket formatting failed"), + expect.any(Error), + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("Error: Administrator creation failed"), + expect.any(Error), + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining( + "Administrator access may be lost, try reformatting DB to restore access", + ), + ); + }); + + test("should log success messages when all operations succeed", async () => { + // Arrange: simulate user confirming and all operations succeeding. + vi.spyOn(helpers, "askUserToContinue").mockResolvedValue(true); + vi.spyOn(helpers, "pingDB").mockResolvedValue(true); + vi.spyOn(helpers, "formatDatabase").mockResolvedValue(true); + vi.spyOn(helpers, "emptyMinioBucket").mockResolvedValue(true); + vi.spyOn(helpers, "ensureAdministratorExists").mockResolvedValue(true); + + const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // Act: call main() + await expect(resetDataModule.main()).resolves.toBeUndefined(); + + // Assert: verify that all success messages are logged. + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining("Database connected successfully"), + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining("Database formatted successfully"), + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining("Bucket formatted successfully"), + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining("Administrator access restored"), + ); + }); +});