diff --git a/database/migrate/.gitignore b/database/migrate/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/database/migrate/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/database/migrate/README.md b/database/migrate/README.md new file mode 100644 index 0000000..cb4ef5c --- /dev/null +++ b/database/migrate/README.md @@ -0,0 +1,9 @@ +# Migrate + +Migrates schemas in the `freecodecamp` database. + +```bash +bun install +cp sample.env .env +bun run index.ts +``` diff --git a/database/migrate/bun.lock b/database/migrate/bun.lock new file mode 100644 index 0000000..c7d9e64 --- /dev/null +++ b/database/migrate/bun.lock @@ -0,0 +1,173 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "migrate", + "dependencies": { + "dotenv": "17", + "mongodb": "6", + "pino": "10.0.0", + "pino-pretty": "13", + }, + "devDependencies": { + "@types/bun": "^1.2.23", + "tsx": "4", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.10", "", { "os": "android", "cpu": "arm" }, "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.10", "", { "os": "android", "cpu": "arm64" }, "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.10", "", { "os": "android", "cpu": "x64" }, "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.10", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.10", "", { "os": "linux", "cpu": "arm" }, "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.10", "", { "os": "linux", "cpu": "ia32" }, "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.10", "", { "os": "none", "cpu": "x64" }, "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.10", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.10", "", { "os": "openbsd", "cpu": "x64" }, "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.10", "", { "os": "sunos", "cpu": "x64" }, "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.10", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="], + + "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.3.1", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg=="], + + "@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="], + + "@types/node": ["@types/node@24.7.0", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw=="], + + "@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="], + + "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], + + "@types/whatwg-url": ["@types/whatwg-url@11.0.5", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ=="], + + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + + "bson": ["bson@6.10.4", "", {}, "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="], + + "bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], + + "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="], + + "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], + + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], + + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], + + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + + "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mongodb": ["mongodb@6.20.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^6.10.4", "mongodb-connection-string-url": "^3.0.2" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.3.2", "socks": "^2.7.1" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ=="], + + "mongodb-connection-string-url": ["mongodb-connection-string-url@3.0.2", "", { "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA=="], + + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "pino": ["pino@10.0.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "slow-redact": "^0.3.0", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA=="], + + "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], + + "pino-pretty": ["pino-pretty@13.1.1", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^3.0.2", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA=="], + + "pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], + + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], + + "slow-redact": ["slow-redact@0.3.1", "", {}, "sha512-NvFvl1GuLZNW4U046Tfi8b26zXo8aBzgCAS2f7yVJR/fArN93mOqSA99cB9uITm92ajSz01bsu1K7SCVVjIMpQ=="], + + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + + "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], + + "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], + + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + + "tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="], + + "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + } +} diff --git a/database/migrate/collections/exam-creator-exam.ts b/database/migrate/collections/exam-creator-exam.ts new file mode 100644 index 0000000..cd565dc --- /dev/null +++ b/database/migrate/collections/exam-creator-exam.ts @@ -0,0 +1,73 @@ +import { Db, ObjectId } from 'mongodb'; +import { log } from '../logger'; + +interface ExamCreatorExam { + _id: ObjectId; + config: { + totalTimeInMS: number; + totalTimeInS?: number | null; + retakeTimeInMS: number; + retakeTimeInS?: number | null; + }; + version: number; +} + +export async function migrate(db: Db) { + const child = log.child({ collection: 'ExamCreatorExam' }); + child.info('Starting migration for ExamCreatorExam collection'); + const collection = db.collection('ExamCreatorExam'); + + const query = { + $or: [ + { 'config.totalTimeInS': { $exists: false } }, + { 'config.retakeTimeInS': { $exists: false } } + ], + version: 1 + }; + + const cursor = collection.find(query, { + projection: { _id: 1, config: 1 } + }); + + const updates: { + _id: ExamCreatorExam['_id']; + totalTimeInS: number; + retakeTimeInS: number; + }[] = []; + + while (await cursor.hasNext()) { + const doc = await cursor.next(); + if (!doc) break; + + const totalTimeInMS = doc.config.totalTimeInMS; + const totalTimeInS = Math.round(totalTimeInMS / 1000); + + const retakeTimeInMS = doc.config.retakeTimeInMS; + const retakeTimeInS = Math.round(retakeTimeInMS / 1000); + + updates.push({ _id: doc._id, totalTimeInS, retakeTimeInS }); + } + + child.info(`Found ${updates.length} docs to migrate.`); + + if (!updates.length) { + return; + } + + // Perform updates in bulk for efficiency + const bulk = collection.initializeUnorderedBulkOp(); + for (const u of updates) { + bulk.find({ _id: u._id, ...query }).updateOne({ + $set: { + 'config.totalTimeInS': u.totalTimeInS, + 'config.retakeTimeInS': u.retakeTimeInS, + version: 2 + } + }); + } + + const result = await bulk.execute(); + child.info( + `Bulk update complete. Matched: ${result.matchedCount}, Modified: ${result.modifiedCount}` + ); +} diff --git a/database/migrate/collections/exam-creator-user.ts b/database/migrate/collections/exam-creator-user.ts new file mode 100644 index 0000000..cb2ea68 --- /dev/null +++ b/database/migrate/collections/exam-creator-user.ts @@ -0,0 +1,64 @@ +import { Db, ObjectId } from "mongodb"; +import { log } from "../logger"; + +interface ExamCreatorUser { + _id: ObjectId; + settings?: ExamCreatorUserSettings; + version: number; +} + +interface ExamCreatorUserSettings { + databaseEnvironment: "Production" | "Staging"; +} + +export async function migrate(db: Db) { + const child = log.child({ collection: "ExamCreatorUser" }); + child.info("Starting migration for ExamCreatorUser collection"); + const collection = db.collection("ExamCreatorUser"); + + const query = { + $or: [{ settings: { $exists: false } }, { version: { $exists: false } }], + }; + + const cursor = collection.find(query, { + projection: { _id: 1, settings: 1 }, + }); + + const updates: { + _id: ExamCreatorUser["_id"]; + settings: ExamCreatorUserSettings; + }[] = []; + + while (await cursor.hasNext()) { + const doc = await cursor.next(); + if (!doc) break; + + const settings: ExamCreatorUserSettings = { + databaseEnvironment: doc.settings?.databaseEnvironment || "Production", + }; + + updates.push({ _id: doc._id, settings }); + } + + child.info(`Found ${updates.length} docs to migrate.`); + + if (!updates.length) { + return; + } + + // Perform updates in bulk for efficiency + const bulk = collection.initializeUnorderedBulkOp(); + for (const u of updates) { + bulk.find({ _id: u._id, ...query }).updateOne({ + $set: { + settings: u.settings, + version: 1, + }, + }); + } + + const result = await bulk.execute(); + child.info( + `Bulk update complete. Matched: ${result.matchedCount}, Modified: ${result.modifiedCount}` + ); +} diff --git a/database/migrate/collections/exam-environment-challenge.ts b/database/migrate/collections/exam-environment-challenge.ts new file mode 100644 index 0000000..e08b47a --- /dev/null +++ b/database/migrate/collections/exam-environment-challenge.ts @@ -0,0 +1,56 @@ +import { Db, ObjectId } from 'mongodb'; +import { log } from '../logger'; + +interface ExamEnvironmentChallenge { + _id: ObjectId; + version?: number; +} + +export async function migrate(db: Db) { + const child = log.child({ collection: 'ExamEnvironmentChallenge' }); + child.info('Starting migration for ExamEnvironmentChallenge collection'); + const collection = db.collection( + 'ExamEnvironmentChallenge' + ); + + const query = { + version: { $exists: false } + }; + + const cursor = collection.find(query, { + projection: { _id: 1 } + }); + + const updates: { + _id: ExamEnvironmentChallenge['_id']; + version: number; + }[] = []; + + while (await cursor.hasNext()) { + const doc = await cursor.next(); + if (!doc) break; + + updates.push({ _id: doc._id, version: 1 }); + } + + child.info(`Found ${updates.length} docs to migrate.`); + + if (!updates.length) { + return; + } + + // Perform updates in bulk for efficiency + const bulk = collection.initializeUnorderedBulkOp(); + for (const u of updates) { + bulk.find({ _id: u._id, ...query }).updateOne({ + $set: { + version: u.version + } + }); + } + + const result = await bulk.execute(); + child.info( + `Bulk update complete. Matched: ${result.matchedCount}, Modified: ${result.modifiedCount}` + ); +} diff --git a/database/migrate/collections/exam-environment-exam-attempt.ts b/database/migrate/collections/exam-environment-exam-attempt.ts new file mode 100644 index 0000000..dac6911 --- /dev/null +++ b/database/migrate/collections/exam-environment-exam-attempt.ts @@ -0,0 +1,91 @@ +import { Db, ObjectId } from 'mongodb'; +import { log } from '../logger'; + +interface ExamEnvironmentExamAttempt { + _id: ObjectId; + questionSets: ExamEnvironmentQuestionSetAttempt[]; + startTimeInMS: number; + startTime?: Date; + version: number; +} + +interface ExamEnvironmentQuestionSetAttempt { + id: ObjectId; + questions: ExamEnvironmentMultipleChoiceQuestionAttempt[]; +} + +interface ExamEnvironmentMultipleChoiceQuestionAttempt { + id: ObjectId; + submissionTimeInMS: number; + submissionTime?: Date; +} + +export async function migrate(db: Db) { + const child = log.child({ collection: 'ExamEnvironmentExamAttempt' }); + child.info('Starting migration for ExamEnvironmentExamAttempt collection'); + const collection = db.collection( + 'ExamEnvironmentExamAttempt' + ); + + const query = { + $and: [ + { startTime: { $exists: false } }, + { 'questionSets.questions.submissionTime': { $exists: false } } + ], + version: 1 + }; + + const cursor = collection.find(query, { + projection: { _id: 1, questionSets: 1, startTimeInMS: 1 } + }); + + const updates: { + _id: ExamEnvironmentExamAttempt['_id']; + questionSets: ExamEnvironmentQuestionSetAttempt[]; + startTime: Date; + }[] = []; + + while (await cursor.hasNext()) { + const doc = await cursor.next(); + if (!doc) break; + + // Add startTime + const startTime = new Date(doc.startTimeInMS); + + for (const qs of doc.questionSets) { + for (const q of qs.questions) { + // Add submissionTime + q.submissionTime = new Date(q.submissionTimeInMS); + } + } + + updates.push({ + _id: doc._id, + startTime, + questionSets: doc.questionSets + }); + } + + child.info(`Found ${updates.length} docs to migrate.`); + + if (!updates.length) { + return; + } + + // Perform updates in bulk for efficiency + const bulk = collection.initializeUnorderedBulkOp(); + for (const u of updates) { + bulk.find({ _id: u._id, ...query }).updateOne({ + $set: { + questionSets: u.questionSets, + startTime: u.startTime, + version: 2 + } + }); + } + + const result = await bulk.execute(); + child.info( + `Bulk update complete. Matched: ${result.matchedCount}, Modified: ${result.modifiedCount}` + ); +} diff --git a/database/migrate/collections/exam-environment-exam-moderation.ts b/database/migrate/collections/exam-environment-exam-moderation.ts new file mode 100644 index 0000000..2505303 --- /dev/null +++ b/database/migrate/collections/exam-environment-exam-moderation.ts @@ -0,0 +1,58 @@ +import { Db, ObjectId } from 'mongodb'; +import { log } from '../logger'; + +interface ExamEnvironmentExamModeration { + _id: ObjectId; + challengesAwarded?: boolean; +} + +export async function migrate(db: Db) { + const child = log.child({ collection: 'ExamEnvironmentExamModeration' }); + child.info('Starting migration for ExamEnvironmentExamModeration collection'); + const collection = db.collection( + 'ExamEnvironmentExamModeration' + ); + + const query = { + challengesAwarded: { $exists: false }, + version: 1 + }; + + const cursor = collection.find(query, { + projection: { _id: 1 } + }); + + const updates: { + _id: ExamEnvironmentExamModeration['_id']; + challengesAwarded: boolean; + }[] = []; + + while (await cursor.hasNext()) { + const doc = await cursor.next(); + if (!doc) break; + + updates.push({ _id: doc._id, challengesAwarded: false }); + } + + child.info(`Found ${updates.length} docs to migrate.`); + + if (!updates.length) { + return; + } + + // Perform updates in bulk for efficiency + const bulk = collection.initializeUnorderedBulkOp(); + for (const u of updates) { + bulk.find({ _id: u._id, ...query }).updateOne({ + $set: { + challengesAwarded: u.challengesAwarded, + version: 2 + } + }); + } + + const result = await bulk.execute(); + child.info( + `Bulk update complete. Matched: ${result.matchedCount}, Modified: ${result.modifiedCount}` + ); +} diff --git a/database/migrate/collections/exam-environment-exam.ts b/database/migrate/collections/exam-environment-exam.ts new file mode 100644 index 0000000..ef33482 --- /dev/null +++ b/database/migrate/collections/exam-environment-exam.ts @@ -0,0 +1,72 @@ +import { Db, ObjectId } from 'mongodb'; +import { log } from '../logger'; + +interface ExamEnvironmentExam { + _id: ObjectId; + config: { + totalTimeInMS: number; + totalTimeInS?: number | null; + retakeTimeInMS: number; + retakeTimeInS?: number | null; + }; +} + +export async function migrate(db: Db) { + const child = log.child({ collection: 'ExamEnvironmentExam' }); + child.info('Starting migration for ExamEnvironmentExam collection'); + const collection = db.collection('ExamEnvironmentExam'); + + const query = { + $or: [ + { 'config.totalTimeInS': { $exists: false } }, + { 'config.retakeTimeInS': { $exists: false } } + ], + version: 1 + }; + + const cursor = collection.find(query, { + projection: { _id: 1, config: 1 } + }); + + const updates: { + _id: ExamEnvironmentExam['_id']; + totalTimeInS: number; + retakeTimeInS: number; + }[] = []; + + while (await cursor.hasNext()) { + const doc = await cursor.next(); + if (!doc) break; + + const totalTimeInMS = doc.config.totalTimeInMS; + const totalTimeInS = Math.round(totalTimeInMS / 1000); + + const retakeTimeInMS = doc.config.retakeTimeInMS; + const retakeTimeInS = Math.round(retakeTimeInMS / 1000); + + updates.push({ _id: doc._id, totalTimeInS, retakeTimeInS }); + } + + child.info(`Found ${updates.length} docs to migrate.`); + + if (!updates.length) { + return; + } + + // Perform updates in bulk for efficiency + const bulk = collection.initializeUnorderedBulkOp(); + for (const u of updates) { + bulk.find({ _id: u._id, ...query }).updateOne({ + $set: { + 'config.totalTimeInS': u.totalTimeInS, + 'config.retakeTimeInS': u.retakeTimeInS, + version: 2 + } + }); + } + + const result = await bulk.execute(); + child.info( + `Bulk update complete. Matched: ${result.matchedCount}, Modified: ${result.modifiedCount}` + ); +} diff --git a/database/migrate/index.ts b/database/migrate/index.ts new file mode 100644 index 0000000..fe02104 --- /dev/null +++ b/database/migrate/index.ts @@ -0,0 +1,42 @@ +import { MongoClient } from "mongodb"; + +import { migrate as migrateExamCreatorUser } from "./collections/exam-creator-user"; +import { migrate as migrateExamCreatorExam } from "./collections/exam-creator-exam"; +import { migrate as migrateExamEnvironmentExam } from "./collections/exam-environment-exam"; +import { migrate as migrateExamEnvironmentChallenge } from "./collections/exam-environment-challenge"; +import { migrate as migrateExamEnvironmentExamAttempt } from "./collections/exam-environment-exam-attempt"; +import { migrate as migrateExamEnvironmentExamModeration } from "./collections/exam-environment-exam-moderation"; + +import { log } from "./logger"; + +const { MONGODB_URI } = process.env; +if (!MONGODB_URI) { + console.error("MONGOHQ_URL env var is required. Aborting."); + process.exit(1); +} + +async function main() { + const client = new MongoClient(MONGODB_URI as string); + try { + await client.db("admin").command({ ping: 1 }); + log.info("Connected to MongoDB"); + const db = client.db("freecodecamp"); + await Promise.all([ + migrateExamCreatorUser(db), + migrateExamCreatorExam(db), + migrateExamEnvironmentExam(db), + migrateExamEnvironmentChallenge(db), + migrateExamEnvironmentExamAttempt(db), + migrateExamEnvironmentExamModeration(db), + ]); + log.info("Migration completed successfully."); + } catch (err) { + log.error("Migration failed:"); + log.error(err); + process.exitCode = 1; + } finally { + await client.close(); + } +} + +main().catch(console.error); diff --git a/database/migrate/logger.ts b/database/migrate/logger.ts new file mode 100644 index 0000000..85acbc1 --- /dev/null +++ b/database/migrate/logger.ts @@ -0,0 +1,23 @@ +import { + pino, + transport, + type DestinationStream, + type TransportTargetOptions, +} from "pino"; + +const prettyTarget: TransportTargetOptions = { + target: "pino-pretty", + options: { + singleLine: true, + translateTime: "HH:MM:ss Z", + ignore: "pid,hostname", + colorize: true, + messageFormat: "{if collection}({collection}){end} {msg}", + }, +}; + +const stream = transport({ + targets: [prettyTarget], +}) as DestinationStream; + +export const log = pino({ level: "info" }, stream); diff --git a/database/migrate/package.json b/database/migrate/package.json new file mode 100644 index 0000000..980c6a4 --- /dev/null +++ b/database/migrate/package.json @@ -0,0 +1,18 @@ +{ + "name": "migrate", + "version": "1.0.0", + "private": true, + "dependencies": { + "dotenv": "17", + "mongodb": "6", + "pino": "10.0.0", + "pino-pretty": "13" + }, + "devDependencies": { + "@types/bun": "1", + "tsx": "4" + }, + "scripts": { + "start": "bun run ./index.ts" + } +} diff --git a/database/migrate/sample.env b/database/migrate/sample.env new file mode 100644 index 0000000..cba242c --- /dev/null +++ b/database/migrate/sample.env @@ -0,0 +1 @@ +MONGODB_URL=mongodb://127.0.0.1:27017/freecodecamp?directConnection=true diff --git a/database/migrate/tsconfig.json b/database/migrate/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/database/migrate/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/database/migrations/.gitkeep b/database/migrations/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/database/migrations/mikoyan/src/main.rs b/database/migrations/mikoyan/src/main.rs deleted file mode 100644 index e69de29..0000000