From b9ac3e5f94cb0cf2d5b4b710a931ecd72210a208 Mon Sep 17 00:00:00 2001 From: liuyi Date: Sat, 21 Jun 2025 14:54:07 +0800 Subject: [PATCH] add db seed handler --- bun.lock | 58 ++--------- package.json | 1 + pnpm-lock.yaml | 3 + src/modules/core/config.ts | 1 + src/modules/core/helpers/time.ts | 30 ++++++ src/modules/core/types.ts | 38 ++++++++ src/modules/database/base/BaseSeeder.ts | 85 ++++++++++++++++ src/modules/database/commands/types.ts | 62 ++++++++++++ src/modules/database/config.ts | 4 +- .../database/resolver/seeder.runner.ts | 37 +++++++ src/modules/database/types.ts | 10 ++ src/modules/database/utils.ts | 96 ++++++++++++++++++- 12 files changed, 374 insertions(+), 51 deletions(-) create mode 100644 src/modules/core/helpers/time.ts create mode 100644 src/modules/database/base/BaseSeeder.ts create mode 100644 src/modules/database/resolver/seeder.runner.ts diff --git a/bun.lock b/bun.lock index bad4c46..1b8938f 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index 30dd6ba..0efade5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45567a5..34fd774 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/modules/core/config.ts b/src/modules/core/config.ts index 5832a1f..f20e9fa 100644 --- a/src/modules/core/config.ts +++ b/src/modules/core/config.ts @@ -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: ( diff --git a/src/modules/core/helpers/time.ts b/src/modules/core/helpers/time.ts new file mode 100644 index 0000000..2cf9156 --- /dev/null +++ b/src/modules/core/helpers/time.ts @@ -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('app'); + const now = dayjs(date, format, locale ?? config.locale, strict).clone(); + return now.tz(zonetime ?? config.timezone); +} diff --git a/src/modules/core/types.ts b/src/modules/core/types.ts index 70acd10..989a788 100644 --- a/src/modules/core/types.ts +++ b/src/modules/core/types.ts @@ -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; } +/** + * 时间参数选项类型 + */ +export interface TimeOptions { + /** + * 时间属性,如果不传入则获取当前属性 + */ + date?: dayjs.ConfigType; + /** + * 输出时间格式 + */ + format?: dayjs.OptionType; + /** + * 语言,如果不传入则使用app配置中设置的默认语言 + */ + locale?: string; + /** + * 是否开启严格模式 + */ + strict?: boolean; + /** + * 时区。如果不传入则使用app配置中设置的默认时区 + */ + zonetime?: string; +} + export interface PanicOption { message: string; diff --git a/src/modules/database/base/BaseSeeder.ts b/src/modules/database/base/BaseSeeder.ts new file mode 100644 index 0000000..8d553a5 --- /dev/null +++ b/src/modules/database/base/BaseSeeder.ts @@ -0,0 +1,85 @@ +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 { + Seeder, + SeederConstructor, + SeederLoadParams, + SeederOptions, +} from '@/modules/database/commands/types'; +import { DBOptions } from '@/modules/database/types'; + +/** + * 数据填充基类 + */ +export abstract class BaseSeeder implements Seeder { + protected connection: string; + protected dataSource: DataSource; + protected em: EntityManager; + protected configure: Configure; + protected ignoreLock: boolean; + protected truncates: EntityTarget[] = []; + + constructor( + protected readonly spinner: Ora, + protected readonly args: SeederOptions, + ) {} + + /** + * 清空原数据并重新加载数据 + * @param params + */ + async load(params: SeederLoadParams): Promise { + const { connection, dataSource, em, configure, ignoreLock } = params; + this.connection = connection; + this.dataSource = dataSource; + this.em = em; + this.configure = configure; + this.ignoreLock = ignoreLock; + + if (this.ignoreLock) { + for (const option of this.truncates) { + await this.em.clear(option); + } + } + + return this.run(this.dataSource); + } + + /** + * 运行seeder的关键方法 + * @param dataSource + * @param em + * @protected + */ + protected abstract run(dataSource: DataSource, em?: EntityManager): Promise; + + protected async getDBConfig() { + const { connections = [] }: DBOptions = await this.configure.get('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, + }); + } +} diff --git a/src/modules/database/commands/types.ts b/src/modules/database/commands/types.ts index 0260545..cca887c 100644 --- a/src/modules/database/commands/types.ts +++ b/src/modules/database/commands/types.ts @@ -1,5 +1,9 @@ +import { Ora } from 'ora'; +import { DataSource, EntityManager } from 'typeorm'; import { Arguments } from 'yargs'; +import { Configure } from '@/modules/config/configure'; + /** * 基础数据库命令参数类型 */ @@ -60,3 +64,61 @@ 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; +} + +/** + * 数据填充类的load函数参数 + */ +export interface SeederLoadParams { + /** + * 数据库连接名称 + */ + connection: string; + /** + * 数据库连接 + */ + dataSource: DataSource; + /** + * EntityManager实例 + */ + em: EntityManager; + /** + * 项目配置类 + */ + configure: Configure; + /** + * 是否忽略锁定 + */ + ignoreLock: boolean; +} diff --git a/src/modules/database/config.ts b/src/modules/database/config.ts index 16cebcc..72e2549 100644 --- a/src/modules/database/config.ts +++ b/src/modules/database/config.ts @@ -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,7 @@ export const createDBConfig: ( register, hook: (configure, value) => createDBOptions(value), defaultRegister: () => ({ - common: { charset: 'utf8mb4', logging: ['error'] }, + common: { charset: 'utf8mb4', logging: ['error'], seeders: [], seedRunner: SeederRunner }, connections: [], }), }); diff --git a/src/modules/database/resolver/seeder.runner.ts b/src/modules/database/resolver/seeder.runner.ts new file mode 100644 index 0000000..4fb82e1 --- /dev/null +++ b/src/modules/database/resolver/seeder.runner.ts @@ -0,0 +1,37 @@ +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'; + +/** + * 默认的Seed Runner + */ +export class SeederRunner extends BaseSeeder { + protected async run(dataSource: DataSource, em?: EntityManager): Promise { + let seeders: Type[] = ((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(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)); + } +} diff --git a/src/modules/database/types.ts b/src/modules/database/types.ts index 0e1f8f7..2018f6e 100644 --- a/src/modules/database/types.ts +++ b/src/modules/database/types.ts @@ -7,6 +7,7 @@ import { TreeRepository, } from 'typeorm'; +import { SeederConstructor } from '@/modules/database/commands/types'; import { OrderType, SelectTrashMode } from '@/modules/database/constants'; import { BaseRepository } from './base/repository'; @@ -92,4 +93,13 @@ type DBAdditionalOption = { * 是否在启动应用后自动运行迁移 */ autoMigrate?: boolean; + + /** + * 数据填充类列表 + */ + seeders?: SeederConstructor[]; + /** + * 数据填充入口类 + */ + seedRunner?: SeederConstructor; }; diff --git a/src/modules/database/utils.ts b/src/modules/database/utils.ts index ed915d0..856bc5f 100644 --- a/src/modules/database/utils.ts +++ b/src/modules/database/utils.ts @@ -2,14 +2,25 @@ 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, + ObjectLiteral, + ObjectType, + Repository, + SelectQueryBuilder, +} from 'typeorm'; import { Configure } from '@/modules/config/configure'; +import { Seeder, SeederConstructor, SeederOptions } from '@/modules/database/commands/types'; import { DBOptions, OrderQueryType, PaginateOptions, PaginateReturn, + TypeormOption, } from '@/modules/database/types'; import { CUSTOM_REPOSITORY_METADATA } from './constants'; @@ -151,3 +162,86 @@ 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 { + 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 { + const seeder: Seeder = new Clazz(spinner, args); + const dataSource: DataSource = new DataSource({ ...dbConfig } as DataSourceOptions); + + await dataSource.initialize(); + 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, + }); + 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, + }); + 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; +}