Compare commits

...

4 Commits

Author SHA1 Message Date
b20b02eb2a add db seed handler 2025-06-21 17:07:09 +08:00
af46a76a9c add db seed handler 2025-06-21 15:05:39 +08:00
b9ac3e5f94 add db seed handler 2025-06-21 14:54:07 +08:00
0191343d10 add db migration 2025-06-21 08:21:21 +08:00
22 changed files with 755 additions and 59 deletions

View File

@ -10,11 +10,11 @@
"@nestjs/platform-fastify": "^11.1.3",
"@nestjs/swagger": "^11.2.0",
"@nestjs/typeorm": "^11.0.0",
"bun": "^1.2.16",
"chalk": "^5.4.1",
"chokidar": "^4.0.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"dayjs": "^1.11.13",
"deepmerge": "^4.3.1",
"dotenv": "^16.5.0",
"find-up": "^7.0.0",
@ -302,7 +302,7 @@
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@lukeed/csprng": ["@lukeed/csprng@1.1.0", "", {}, "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA=="],
@ -374,28 +374,6 @@
"@nuxt/opencollective": ["@nuxt/opencollective@0.4.1", "", { "dependencies": { "consola": "^3.2.3" }, "bin": { "opencollective": "bin/opencollective.js" } }, "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ=="],
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.2.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NLVU9YDORq/3WuJOE5TQv5of3R99n56gYZPfdqP4U0/5nllbC8yzRxA2BWwAS2RxxD0Y3bxqEVUsIGiTNN2jxg=="],
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.2.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-HpcSVCTH9n+9bG2zu3OUJ9h22m6HzNgZpqib9r4NEVZg7Z2U86bOUMKlTCA0ZANaWsK9czl2VIhMWbLF4fgvLA=="],
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.2.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-FtKr6FwLN+QfrF0/vJtOwBMU72krmrHlxhRSElbKEOWox2n2vWSZ/sNNkHePEsrxGfqaHC5GhEZk2lnaZTavBQ=="],
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.2.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-nd0eZhihfgrDtfI/NdEqOAQ8KY87SWNQLZKjRB8WoYkqcY1BGwtZqvJOc2bEn2oERJ8K2etJRynXz+MKngiYxw=="],
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.2.16", "", { "os": "linux", "cpu": "none" }, "sha512-MhvQ0hecunZnbac9cEOqA1CGk/ISDhhnF35i9l90Jgc/osfgGndViLkMp3wk1EO5UG4/Kbil1OlfLmyOHKq0SQ=="],
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.2.16", "", { "os": "linux", "cpu": "x64" }, "sha512-qYUXPXbT4S+MImv51+dLBHKFYy40QIowwCRtzUFGf3TG+9MQQUXHNXryMNSdHveHqecd9rO1EIQ8hroAPBl+Sg=="],
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.2.16", "", { "os": "linux", "cpu": "x64" }, "sha512-ZysDeqDfUAqKrQu2R+ddRgSCY30qSnn0LQLr6fAm7Pw9lU2yhWVNa8R3DavddmZQc1vUw6j3ITIAE+DDT9OBCg=="],
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.2.16", "", { "os": "linux", "cpu": "x64" }, "sha512-6o5Oi5ARKYErF6nIBrewxtl20PGhM97faPemJ+v26D47dRNAlUWN5lMVuOqZOhYjqzOe4V+NpxIFBHtXWEmoNQ=="],
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.2.16", "", { "os": "linux", "cpu": "x64" }, "sha512-cWwny3cxYkvV9fYnSDb2brXodWV7IcG+Bwd3q3b8OUYbeC3ekHN3zm+TYdSxIVhMm7z46CkiDz5QnnQWVVfZ5A=="],
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.2.16", "", { "os": "win32", "cpu": "x64" }, "sha512-1xUlHHbMZ3DMZlEcppBAQ5vQDgNHDMIGB/AXO+dxQJl/3GiO/Ek4pMDzcqMnlbGDaDcTmTXyZ6cEXEF4C2qygQ=="],
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.2.16", "", { "os": "win32", "cpu": "x64" }, "sha512-tHdtHqH6c5ScNusLWOzZCTeuV2rSc3mvlLQQ+DYefTy+XwtjXmY47MbBSgNuBWVYePIob9BqDFOtTHYIWRZTww=="],
"@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.2.2", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
@ -760,8 +738,6 @@
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun": ["bun@1.2.16", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.2.16", "@oven/bun-darwin-x64": "1.2.16", "@oven/bun-darwin-x64-baseline": "1.2.16", "@oven/bun-linux-aarch64": "1.2.16", "@oven/bun-linux-aarch64-musl": "1.2.16", "@oven/bun-linux-x64": "1.2.16", "@oven/bun-linux-x64-baseline": "1.2.16", "@oven/bun-linux-x64-musl": "1.2.16", "@oven/bun-linux-x64-musl-baseline": "1.2.16", "@oven/bun-windows-x64": "1.2.16", "@oven/bun-windows-x64-baseline": "1.2.16" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bun.exe" } }, "sha512-sjZH6rr1P6yu44+XPA8r+ZojwmK9Kbz9lO6KAA/4HRIupdpC31k7b93crLBm19wEYmd6f2+3+57/7tbOcmHbGg=="],
"bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="],
"cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="],
@ -1948,7 +1924,7 @@
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
@ -1964,8 +1940,6 @@
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="],
"@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@angular-devkit/core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@angular-devkit/core/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="],
@ -1978,14 +1952,14 @@
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
@ -2038,8 +2012,6 @@
"@jest/fake-timers/jest-util": ["jest-util@30.0.0", "", { "dependencies": { "@jest/types": "30.0.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-fhNBBM9uSUbd4Lzsf8l/kcAdaHD/4SgoI48en3HXcBEMwKwoleKFMZ6cYEYs21SB779PRuRCyNLmymApAm8tZw=="],
"@jest/reporters/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@jest/reporters/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@jest/reporters/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
@ -2052,10 +2024,6 @@
"@jest/snapshot-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@jest/source-map/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@jest/transform/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@jest/transform/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@jest/transform/jest-util": ["jest-util@30.0.0", "", { "dependencies": { "@jest/types": "30.0.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-fhNBBM9uSUbd4Lzsf8l/kcAdaHD/4SgoI48en3HXcBEMwKwoleKFMZ6cYEYs21SB779PRuRCyNLmymApAm8tZw=="],
@ -2064,10 +2032,6 @@
"@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@jridgewell/source-map/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@nestjs/cli/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
"@nestjs/schematics/@angular-devkit/core": ["@angular-devkit/core@19.2.6", "", { "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", "rxjs": "7.8.1", "source-map": "0.7.4" }, "peerDependencies": { "chokidar": "^4.0.0" }, "optionalPeers": ["chokidar"] }, "sha512-WFgiYhrDMq83UNaGRAneIM7CYYdBozD+yYA9BjoU8AgBLKtrvn6S8ZcjKAk5heoHtY/u8pEb0mwDTz9gxFmJZQ=="],
@ -2174,8 +2138,6 @@
"ip-address/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"istanbul-lib-source-maps/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"jake/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"jest-changed-files/jest-util": ["jest-util@30.0.0", "", { "dependencies": { "@jest/types": "30.0.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-fhNBBM9uSUbd4Lzsf8l/kcAdaHD/4SgoI48en3HXcBEMwKwoleKFMZ6cYEYs21SB779PRuRCyNLmymApAm8tZw=="],
@ -2332,8 +2294,6 @@
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"terser-webpack-plugin/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"terser-webpack-plugin/schema-utils": ["schema-utils@4.3.2", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ=="],
"test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
@ -2348,8 +2308,6 @@
"unbzip2-stream/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"v8-to-istanbul/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"vizion/async": ["async@2.6.4", "", { "dependencies": { "lodash": "^4.17.14" } }, "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA=="],
"webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
@ -2382,8 +2340,6 @@
"@angular-devkit/schematics/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@fastify/ajv-compiler/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@fastify/static/glob/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
@ -2712,6 +2668,10 @@
"@nestjs/schematics/@angular-devkit/schematics/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@pm2/agent/semver/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"@pm2/io/semver/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"@xhmikosr/archive-type/file-type/get-stream/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
"@xhmikosr/decompress-tar/file-type/get-stream/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],

View File

@ -33,6 +33,7 @@
"chokidar": "^4.0.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"dayjs": "^1.11.13",
"deepmerge": "^4.3.1",
"dotenv": "^16.5.0",
"find-up": "^7.0.0",

View File

@ -38,6 +38,9 @@ importers:
class-validator:
specifier: ^0.14.2
version: 0.14.2
dayjs:
specifier: ^1.11.13
version: 1.11.13
deepmerge:
specifier: ^4.3.1
version: 4.3.1

View File

@ -14,6 +14,7 @@ export const getDefaultAppConfig = (configure: Configure) => ({
https: configure.env.get('APP_SSL', (v) => toBoolean(v), false),
locale: configure.env.get('APP_LOCALE', 'zh_CN'),
fallbackLocale: configure.env.get('APP_FALLBACK_LOCALE', 'en'),
timezone: configure.env.get('APP_TIMEZONE', 'Asia/Shanghai'),
});
export const createAppConfig: (

View File

@ -0,0 +1,30 @@
import dayjs from 'dayjs';
import advancedFormat from 'dayjs/plugin/advancedFormat';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import dayOfYear from 'dayjs/plugin/dayOfYear';
import localeData from 'dayjs/plugin/localeData';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import { Configure } from '@/modules/config/configure';
import { AppConfig, TimeOptions } from '@/modules/core/types';
dayjs.extend(localeData);
dayjs.extend(utc);
dayjs.extend(advancedFormat);
dayjs.extend(customParseFormat);
dayjs.extend(dayOfYear);
dayjs.extend(timezone);
/**
* dayjs时间对象
* @param configure
* @param options
*/
export async function getTime(configure: Configure, options?: TimeOptions) {
const { date, format, locale, strict, zonetime } = options ?? {};
const config = await configure.get<AppConfig>('app');
const now = dayjs(date, format, locale ?? config.locale, strict).clone();
return now.tz(zonetime ?? config.timezone);
}

View File

@ -1,6 +1,7 @@
import { ModuleMetadata, PipeTransform, Type } from '@nestjs/common';
import { NestFastifyApplication } from '@nestjs/platform-fastify';
import dayjs from 'dayjs';
import { Ora } from 'ora';
import { StartOptions } from 'pm2';
import { CommandModule } from 'yargs';
@ -53,10 +54,21 @@ export interface AppConfig {
https: boolean;
/**
* ,zh-cn
*/
locale: string;
/**
*
*/
fallbackLocale: string;
/**
* ,Asia/Shanghai
*/
timezone: string;
url?: string;
prefix?: string;
@ -64,6 +76,32 @@ export interface AppConfig {
pm2?: Omit<StartOptions, 'name' | 'cwd' | 'script' | 'args' | 'interpreter' | 'watch'>;
}
/**
*
*/
export interface TimeOptions {
/**
*
*/
date?: dayjs.ConfigType;
/**
*
*/
format?: dayjs.OptionType;
/**
* 使app配置中设置的默认语言
*/
locale?: string;
/**
*
*/
strict?: boolean;
/**
* 使app配置中设置的默认时区
*/
zonetime?: string;
}
export interface PanicOption {
message: string;

View File

@ -0,0 +1,96 @@
import { isNil } from 'lodash';
import { Ora } from 'ora';
import { DataSource, EntityManager, EntityTarget, ObjectLiteral } from 'typeorm';
import { Configure } from '@/modules/config/configure';
import { panic } from '@/modules/core/helpers';
import {
DBFactory,
Seeder,
SeederConstructor,
SeederLoadParams,
SeederOptions,
} from '@/modules/database/commands/types';
import { DBFactoryOption, DBOptions } from '@/modules/database/types';
import { factoryBuilder } from '@/modules/database/utils';
/**
*
*/
export abstract class BaseSeeder implements Seeder {
protected connection: string;
protected dataSource: DataSource;
protected em: EntityManager;
protected configure: Configure;
protected ignoreLock: boolean;
protected truncates: EntityTarget<ObjectLiteral>[] = [];
protected factories: { [entityName: string]: DBFactoryOption<any, any> };
constructor(
protected readonly spinner: Ora,
protected readonly args: SeederOptions,
) {}
/**
*
* @param params
*/
async load(params: SeederLoadParams): Promise<any> {
const { connection, dataSource, em, configure, ignoreLock, factory, factories } = params;
this.connection = connection;
this.dataSource = dataSource;
this.em = em;
this.configure = configure;
this.ignoreLock = ignoreLock;
this.factories = factories;
if (this.ignoreLock) {
for (const option of this.truncates) {
await this.em.clear(option);
}
}
return this.run(factory, this.dataSource);
}
/**
* seeder的关键方法
* @param factory
* @param dataSource
* @param em
* @protected
*/
protected abstract run(
factory?: DBFactory,
dataSource?: DataSource,
em?: EntityManager,
): Promise<any>;
protected async getDBConfig() {
const { connections = [] }: DBOptions = await this.configure.get<DBOptions>('database');
const dbConfig = connections.find(({ name }) => name === this.connection);
if (isNil(dbConfig)) {
await panic(`Database connection named ${this.connection} not exists!`);
}
return dbConfig;
}
/**
* seeder
* @param SubSeeder
* @protected
*/
protected async call(SubSeeder: SeederConstructor) {
const subSeeder: Seeder = new SubSeeder(this.spinner, this.args);
await subSeeder.load({
connection: this.connection,
dataSource: this.dataSource,
em: this.em,
configure: this.configure,
ignoreLock: this.ignoreLock,
factories: this.factories,
factory: factoryBuilder(this.configure, this.dataSource, this.factories),
});
}
}

View File

@ -2,3 +2,4 @@ export * from './migration.create.command';
export * from './migration.revert.command';
export * from './migration.generate.command';
export * from './migration.run.command';
export * from './seeder.command';

View File

@ -30,6 +30,7 @@ export async function MigrationGenerateHandler(
}
console.log();
const runner = new TypeormMigrationGenerate();
console.log('dbConfig', dbConfig);
const dataSource = new DataSource({ ...dbConfig } as DataSourceOptions);
console.log();
await runner.handler({

View File

@ -0,0 +1,36 @@
import { Arguments } from 'yargs';
import { CommandItem } from '@/modules/core/types';
import { SeederHandler } from '@/modules/database/commands/seeder.handler';
import { SeederArguments } from '@/modules/database/commands/types';
export const SeederCommand: CommandItem<any, SeederArguments> = async ({ configure }) => ({
command: ['db:seed', 'dbs'],
describe: 'Runs all seeds data.',
builder: {
clear: {
type: 'boolean',
alias: 'r',
describe: 'Clear which tables will truncated specified by seeder class.',
default: true,
},
connection: {
type: 'string',
alias: 'c',
describe: 'Connection name of typeorm to connect database.',
},
transaction: {
type: 'boolean',
alias: 't',
describe: 'If is seed data in transaction,default is true',
default: true,
},
ignorelock: {
type: 'boolean',
alias: 'i',
describe: 'Ignore seed lock and reset all seeds, not do it in production',
default: false,
},
} as const,
handler: async (args: Arguments<SeederArguments>) => SeederHandler(configure, args),
});

View File

@ -0,0 +1,29 @@
import chalk from 'chalk';
import { isNil } from 'lodash';
import ora from 'ora';
import { Configure } from '@/modules/config/configure';
import { panic } from '@/modules/core/helpers';
import { SeederOptions } from '@/modules/database/commands/types';
import { DBOptions } from '@/modules/database/types';
import { runSeeder } from '@/modules/database/utils';
export async function SeederHandler(configure: Configure, args: SeederOptions) {
const cname = args.connection ?? 'default';
const { connections = [] }: DBOptions = await configure.get<DBOptions>('database');
const dbConfig = connections.find(({ name }) => name === cname);
if (isNil(dbConfig)) {
await panic(`Database connection named ${cname} not exists!`);
}
const runner = dbConfig.seedRunner;
const spinner = ora('Start run seeder...');
try {
spinner.start();
await runSeeder(runner, args, spinner, configure, dbConfig);
spinner.succeed(`\n 👍 ${chalk.greenBright.underline(`Finished Seeding`)}`);
} catch (error) {
await panic({ spinner, message: `Run seeder failed`, error });
}
}

View File

@ -22,9 +22,8 @@ export class TypeormMigrationCreate {
const fileName = `${timestamp}-${args.name}`;
const filePath = `${directory}/${fileName}`;
await CommandUtils.createFile(`${filePath}.ts`, fileContent);
console.log(
`Migration ${chalk.blue(`${filePath}.ts`)} has been generated successfully.`,
);
console.log();
console.log(`Migration ${chalk.blue(`${filePath}.ts`)} has been created successfully.`);
} catch (e) {
PlatformTools.logCmdErr('Error during migration creation:', e);
process.exit(1);

View File

@ -96,7 +96,7 @@ export class TypeormMigrationGenerate {
);
} else {
await CommandUtils.createFile(filePath, fileContent);
console.log();
console.log(
chalk.green(
`Migration ${chalk.blue(filePath)} has been generated successfully.`,

View File

@ -1,5 +1,11 @@
import { Ora } from 'ora';
import { DataSource, EntityManager, EntityTarget } from 'typeorm';
import { Arguments } from 'yargs';
import { Configure } from '@/modules/config/configure';
import { DataFactory } from '@/modules/database/resolver/data.factory';
import { DBFactoryOption } from '@/modules/database/types';
/**
*
*/
@ -60,3 +66,97 @@ export interface MigrationRevertOptions {
*
*/
export type MigrationRevertArguments = TypeOrmArguments & MigrationRevertOptions;
/**
*
*/
export interface SeederOptions {
/**
*
*/
connection?: string;
/**
*
*/
transaction?: boolean;
/**
*
*/
ignorelock?: boolean;
}
/**
*
*/
export interface SeederConstructor {
new (spinner: Ora, args: SeederOptions): Seeder;
}
/**
*
*/
export interface Seeder {
load: (params: SeederLoadParams) => Promise<void>;
}
/**
* load函数参数
*/
export interface SeederLoadParams {
/**
*
*/
connection: string;
/**
*
*/
dataSource: DataSource;
/**
* EntityManager实例
*/
em: EntityManager;
/**
*
*/
configure: Configure;
/**
*
*/
ignoreLock: boolean;
/**
* Factory解析器
*/
factory?: DBFactory;
/**
* Factory函数列表
*/
factories: FactoryOptions;
}
/**
*
*/
export type SeederArguments = TypeOrmArguments & SeederOptions;
/**
* Factory解析器
*/
export interface DBFactory {
<P>(entity: EntityTarget<P>): <T>(options?: T) => DataFactory<P, T>;
}
/**
*
*/
export type FactoryOptions = {
[entityName: string]: DBFactoryOption<any, any>;
};
/**
* Factory构造器
*/
export type DBFactoryBuilder = (
configure: Configure,
dataSource: DataSource,
factories: { [entityName: string]: DBFactoryOption<any, any> },
) => DBFactory;

View File

@ -1,5 +1,7 @@
import { resolve } from 'path';
import { SeederRunner } from '@/modules/database/resolver/seeder.runner';
import { ConfigureFactory, ConfigureRegister } from '../config/types';
import { createConnectionOptions } from '../config/utils';
import { deepMerge } from '../core/helpers';
@ -12,7 +14,13 @@ export const createDBConfig: (
register,
hook: (configure, value) => createDBOptions(value),
defaultRegister: () => ({
common: { charset: 'utf8mb4', logging: ['error'] },
common: {
charset: 'utf8mb4',
logging: ['error'],
seeders: [],
seedRunner: SeederRunner,
factories: [],
},
connections: [],
}),
});
@ -23,6 +31,7 @@ export const createDBOptions = (options: DBConfig) => {
{
charset: 'utf8mb4',
logging: ['error'],
autoMigrate: true,
paths: { migration: resolve(__dirname, '../../database/migrations') },
},
options.common ?? {},

View File

@ -21,7 +21,7 @@ type Condition = {
@ValidatorConstraint({ name: 'treeDataUniqueExist', async: true })
@Injectable()
export class TreeUniqueExistContraint implements ValidatorConstraintInterface {
export class TreeUniqueExistConstraint implements ValidatorConstraintInterface {
constructor(private dataSource: DataSource) {}
async validate(value: any, args: ValidationArguments) {
@ -93,7 +93,7 @@ export function IsTreeUniqueExist(
propertyName,
options: validationOptions,
constraints: [params],
validator: TreeUniqueExistContraint,
validator: TreeUniqueExistConstraint,
});
};
}

View File

@ -5,6 +5,8 @@ import { DataSource, ObjectType } from 'typeorm';
import { CUSTOM_REPOSITORY_METADATA } from '@/modules/database/constants';
import { AutoMigrateResolver } from '@/modules/database/resolver/auto.migrate';
import { Configure } from '../config/configure';
import { panic } from '../core/helpers';
@ -12,7 +14,7 @@ import { panic } from '../core/helpers';
import {
DataExistConstraint,
TreeUniqueConstraint,
TreeUniqueExistContraint,
TreeUniqueExistConstraint,
UniqueConstraint,
UniqueExistConstraint,
} from './constraints';
@ -34,7 +36,8 @@ export class DatabaseModule {
UniqueConstraint,
UniqueExistConstraint,
TreeUniqueConstraint,
TreeUniqueExistContraint,
TreeUniqueExistConstraint,
AutoMigrateResolver,
];
return {
global: true,

View File

@ -0,0 +1,52 @@
import { join } from 'path';
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { DataSource, DataSourceOptions } from 'typeorm';
import { Configure } from '@/modules/config/configure';
import { panic } from '@/modules/core/helpers';
import { TypeormMigrationRun } from '@/modules/database/commands/typeorm.migration.run';
import { DBOptions } from '@/modules/database/types';
@Injectable()
export class AutoMigrateResolver {
constructor(private ref: ModuleRef) {}
async onModuleInit() {
const configure = this.ref.get(Configure, { strict: false });
const { connections = [] }: DBOptions = await configure.get<DBOptions>('database');
for (const conn of connections) {
let dataSource: DataSource | undefined;
if (conn.autoMigrate) {
try {
dataSource = new DataSource(conn as DataSourceOptions);
const runner = new TypeormMigrationRun();
if (dataSource && dataSource.isInitialized) {
await dataSource.destroy();
}
dataSource.setOptions({
subscribers: [],
synchronize: false,
migrationsRun: false,
logging: ['error'],
migrations: [
join(conn.paths.migration, '**/*.ts'),
join(conn.paths.migration, '**/*.js'),
],
dropSchema: false,
});
await dataSource.initialize();
await runner.handler({ dataSource });
} catch (error) {
await panic({ message: 'Run migrations failed!', error });
} finally {
if (dataSource && dataSource.isInitialized) {
await dataSource.destroy();
}
}
}
}
}
}

View File

@ -0,0 +1,107 @@
import { isPromise } from 'node:util/types';
import { isNil } from 'lodash';
import { EntityManager, EntityTarget } from 'typeorm';
import { panic } from '@/modules/core/helpers';
import { DBFactoryHandler, FactoryOverride } from '@/modules/database/types';
export class DataFactory<P, T> {
private mapFunction!: (entity: P) => Promise<P>;
constructor(
public name: string,
public config: Configure,
public entity: EntityTarget<P>,
protected em: EntityManager,
protected factory: DBFactoryHandler<P, T>,
protected settings: T,
) {}
map(mapFunction: (entity: P) => Promise<P>): DataFactory<P, T> {
this.mapFunction = mapFunction;
return this;
}
async make(params: FactoryOverride<P> = {}): Promise<P> {
if (this.factory) {
let entity: P = await this.resolveEntity(
await this.factory(this.configure, this.settings),
);
if (this.mapFunction) {
entity = await this.mapFunction(entity);
}
for (const key in params) {
if (params[key]) {
entity[key] = params[key];
}
}
return entity;
}
throw new Error('Could not found entity');
}
async create(params: FactoryOverride<P> = {}, existsCheck?: string): Promise<P> {
try {
const entity = await this.make(params);
if (!isNil(existsCheck)) {
const repo = this.em.getRepository(this.entity);
const value = (entity as any)[existsCheck];
if (!isNil(value)) {
const item = await repo.findOneBy({ [existsCheck]: value } as any);
if (isNil(item)) {
return await this.em.save(entity);
}
return item;
}
}
return await this.em.save(entity);
} catch (error) {
const message = 'Could not save entity';
await panic({ message, error });
throw new Error(message);
}
}
async makeMany(amount: number, params: FactoryOverride<P> = {}): Promise<P[]> {
const list = [];
for (let i = 0; i < amount; i++) {
list[i] = await this.make(params);
}
return list;
}
async createMany(
amount: number,
params: FactoryOverride<P> = {},
existsCheck?: string,
): Promise<P[]> {
const list = [];
for (let i = 0; i < amount; i++) {
list[i] = await this.create(params, existsCheck);
}
return list;
}
private async resolveEntity(entity: P): Promise<P> {
for (const attr in entity) {
if (entity[attr]) {
if (isPromise(entity[attr])) {
entity[attr] = await entity[attr];
} else if (typeof entity[attr] === 'object' && !(entity[attr] instanceof Date)) {
const item = entity[attr];
try {
if (typeof (item as any).make === 'function') {
entity[attr] = await (item as any).make();
}
} catch (error) {
const message = `Could not make ${(subEntityFactory as any).name}`;
await panic({ message, error });
throw new Error(message);
}
}
}
}
return entity;
}
}

View File

@ -0,0 +1,42 @@
import { resolve } from 'path';
import { Type } from '@nestjs/common';
import { ensureFileSync, readFileSync, writeFileSync } from 'fs-extra';
import { get, isNil, set } from 'lodash';
import { DataSource, EntityManager } from 'typeorm';
import YAML from 'yaml';
import { BaseSeeder } from '@/modules/database/base/BaseSeeder';
import { DBFactory } from '@/modules/database/commands/types';
/**
* Seed Runner
*/
export class SeederRunner extends BaseSeeder {
protected async run(
factory: DBFactory,
dataSource: DataSource,
em: EntityManager,
): Promise<any> {
let seeders: Type<any>[] = ((await this.getDBConfig()) as any).seeders ?? [];
const seedLockFile = resolve(__dirname, '../../../..', 'seed-lock.yml');
ensureFileSync(seedLockFile);
const lockFileYml = YAML.parse(readFileSync(seedLockFile, 'utf8'));
const locked = isNil(lockFileYml) ? {} : lockFileYml;
const lockNames = get<string[]>(locked, this.connection, []);
if (!this.ignoreLock) {
seeders = seeders.filter((s) => !lockNames.includes(s.name));
}
for (const seeder of seeders) {
await this.call(seeder);
}
set(
locked,
this.connection,
this.ignoreLock
? seeders.map((s) => s.name)
: [...lockNames, ...seeders.map((s) => s.name)],
);
writeFileSync(seedLockFile, JSON.stringify(locked, null, 4));
}
}

View File

@ -2,11 +2,14 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import {
FindTreeOptions,
ObjectLiteral,
ObjectType,
Repository,
SelectQueryBuilder,
TreeRepository,
} from 'typeorm';
import { Configure } from '@/modules/config/configure';
import { SeederConstructor } from '@/modules/database/commands/types';
import { OrderType, SelectTrashMode } from '@/modules/database/constants';
import { BaseRepository } from './base/repository';
@ -87,4 +90,38 @@ type DBAdditionalOption = {
paths?: {
migration?: string;
};
/**
*
*/
autoMigrate?: boolean;
/**
*
*/
seeders?: SeederConstructor[];
/**
*
*/
seedRunner?: SeederConstructor;
/**
*
*/
factories?: (() => DBFactoryOption<any, any>)[];
};
export type DBFactoryHandler<P, T> = (configure: Configure, options: T) => Promise<P>;
export type DBFactoryOption<P, T> = {
entity: ObjectType<P>;
handler: DBFactoryHandler<P, T>;
};
export type DefineFactory = <P, T>(
entity: ObjectType<P>,
handler: DBFactoryHandler<P, T>,
) => () => DBFactoryOption<P, T>;
export type FactoryOverride<Entity> = {
[Property in keyof Entity]: Entity[Property];
};

View File

@ -2,14 +2,34 @@ import { Type } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type';
import { isArray, isNil } from 'lodash';
import { DataSource, ObjectLiteral, ObjectType, Repository, SelectQueryBuilder } from 'typeorm';
import { Ora } from 'ora';
import {
DataSource,
DataSourceOptions,
EntityManager,
EntityTarget,
ObjectLiteral,
ObjectType,
Repository,
SelectQueryBuilder,
} from 'typeorm';
import { Configure } from '@/modules/config/configure';
import {
DBFactoryBuilder,
FactoryOptions,
Seeder,
SeederConstructor,
SeederOptions,
} from '@/modules/database/commands/types';
import { DataFactory } from '@/modules/database/resolver/data.factory';
import {
DBOptions,
DefineFactory,
OrderQueryType,
PaginateOptions,
PaginateReturn,
TypeormOption,
} from '@/modules/database/types';
import { CUSTOM_REPOSITORY_METADATA } from './constants';
@ -151,3 +171,134 @@ export async function addSubscribers(
configure.set('database.connections', newSubscribers);
return subscribers;
}
/**
*
* @param em EntityManager实例
* @param type
* @param disabled
*/
export async function resetForeignKey(
em: EntityManager,
type = 'mysql',
disabled = true,
): Promise<EntityManager> {
let key: string;
let query: string;
if (type === 'sqlite') {
key = disabled ? 'OFF' : 'ON';
query = `PRAGMA foreign_keys = ${key}`;
} else {
key = disabled ? '0' : '1';
query = `SET FOREIGN_KEY_CHECKS = ${key}`;
}
await em.query(query);
return em;
}
/**
*
* @param Clazz
* @param args
* @param spinner Ora雪碧图标
* @param configure
* @param dbConfig
*/
export async function runSeeder(
Clazz: SeederConstructor,
args: SeederOptions,
spinner: Ora,
configure: Configure,
dbConfig: TypeormOption,
): Promise<DataSource> {
const seeder: Seeder = new Clazz(spinner, args);
const dataSource: DataSource = new DataSource({ ...dbConfig } as DataSourceOptions);
await dataSource.initialize();
const factoryMaps: FactoryOptions = {};
for (const factory of dbConfig.factories) {
const { entity, handler } = factory();
factoryMaps[entity.name] = { entity, handler };
}
if (typeof args.transaction === 'boolean' && !args.transaction) {
const em = await resetForeignKey(dataSource.manager, dataSource.options.type);
await seeder.load({
dataSource,
em,
configure,
connection: args.connection ?? 'default',
ignoreLock: args.ignorelock,
factory: factoryBuilder(configure, dataSource, factoryMaps),
factories: factoryMaps,
});
await resetForeignKey(em, dataSource.options.type, false);
} else {
// 在事务中运行
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const em = await resetForeignKey(dataSource.manager, dataSource.options.type);
await seeder.load({
dataSource,
em,
configure,
connection: args.connection ?? 'default',
ignoreLock: args.ignorelock,
factory: factoryBuilder(configure, dataSource, factoryMaps),
factories: factoryMaps,
});
await resetForeignKey(em, dataSource.options.type, false);
await queryRunner.commitTransaction();
} catch (e) {
console.error(e);
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}
}
if (dataSource && dataSource.isInitialized) {
await dataSource.destroy();
}
return dataSource;
}
/**
* factory用于生成数据
* @param entity
* @param handler
*/
export const defineFactory: DefineFactory = (entity, handler) => () => ({ entity, handler });
/**
* Entity类名
* @param entity
*/
export function entityName<T>(entity: EntityTarget<T>): string {
if (isNil(entity)) {
throw new Error('Entity is not defined');
}
if (entity instanceof Function) {
return entity.name;
}
return new (entity as any)().constructor.name;
}
export const factoryBuilder: DBFactoryBuilder =
(configure, dataSource, factories) => (entity) => (settings) => {
const name = entityName(entity);
if (!factories[name]) {
throw new Error(`has none factory for entity named ${name}`);
}
return new DataFactory(
name,
configure,
entity,
dataSource.createEntityManager(),
factories[name].handler,
settings,
);
};