251125:0000 Phase 6 wait start dev Check

This commit is contained in:
2025-11-25 00:28:33 +07:00
parent 0e5d7e7e9e
commit 0ce895c96a
22 changed files with 3757 additions and 489 deletions

View File

@@ -22,6 +22,7 @@
"dependencies": {
"@casl/ability": "^6.7.3",
"@elastic/elasticsearch": "^8.11.1",
"@nestjs-modules/ioredis": "^2.0.2",
"@nestjs/axios": "^4.0.1",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/cache-manager": "^3.0.1",

470
backend/pnpm-lock.yaml generated
View File

@@ -14,6 +14,12 @@ importers:
'@elastic/elasticsearch':
specifier: ^8.11.1
version: 8.19.1
'@nestjs-modules/ioredis':
specifier: ^2.0.2
version: 2.0.2(@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))))(ioredis@5.8.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
'@nestjs/axios':
specifier: ^4.0.1
version: 4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)
'@nestjs/bullmq':
specifier: ^11.0.4
version: 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.63.2)
@@ -53,6 +59,9 @@ importers:
'@nestjs/swagger':
specifier: ^11.2.3
version: 11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
'@nestjs/terminus':
specifier: ^11.0.0
version: 11.0.0(@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
'@nestjs/throttler':
specifier: ^6.4.0
version: 6.4.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)
@@ -71,6 +80,9 @@ importers:
ajv-formats:
specifier: ^3.0.1
version: 3.0.1(ajv@8.17.1)
async-retry:
specifier: ^1.3.3
version: 1.3.3
axios:
specifier: ^1.13.2
version: 1.13.2
@@ -110,15 +122,24 @@ importers:
mysql2:
specifier: ^3.15.3
version: 3.15.3
nest-winston:
specifier: ^1.10.2
version: 1.10.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(winston@3.18.3)
nodemailer:
specifier: ^7.0.10
version: 7.0.10
opossum:
specifier: ^9.0.0
version: 9.0.0
passport:
specifier: ^0.7.0
version: 0.7.0
passport-jwt:
specifier: ^4.0.1
version: 4.0.1
prom-client:
specifier: ^15.1.3
version: 15.1.3
redlock:
specifier: 5.0.0-beta.2
version: 5.0.0-beta.2
@@ -140,6 +161,9 @@ importers:
uuid:
specifier: ^13.0.0
version: 13.0.0
winston:
specifier: ^3.18.3
version: 3.18.3
devDependencies:
'@eslint/eslintrc':
specifier: ^3.2.0
@@ -156,6 +180,9 @@ importers:
'@nestjs/testing':
specifier: ^11.0.1
version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)
'@types/async-retry':
specifier: ^1.4.9
version: 1.4.9
'@types/bcrypt':
specifier: ^6.0.0
version: 6.0.0
@@ -180,6 +207,9 @@ importers:
'@types/node':
specifier: ^22.10.7
version: 22.19.1
'@types/opossum':
specifier: ^8.1.9
version: 8.1.9
'@types/passport-jwt':
specifier: ^4.0.1
version: 4.0.1
@@ -572,10 +602,17 @@ packages:
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
'@colors/colors@1.6.0':
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
engines: {node: '>=0.1.90'}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@dabh/diagnostics@2.0.8':
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==}
'@elastic/elasticsearch@8.19.1':
resolution: {integrity: sha512-+1j9NnQVOX+lbWB8LhCM7IkUmjU05Y4+BmSLfusq0msCsQb1Va+OUKFCoOXjCJqQrcgdRdQCjYYyolQ/npQALQ==}
engines: {node: '>=18'}
@@ -980,6 +1017,20 @@ packages:
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
'@nestjs-modules/ioredis@2.0.2':
resolution: {integrity: sha512-8pzSvT8R3XP6p8ZzQmEN8OnY0yWrJ/elFhwQK+PID2zf1SLBkAZ18bDcx3SKQ2atledt0gd9kBeP5xT4MlyS7Q==}
peerDependencies:
'@nestjs/common': '>=6.7.0'
'@nestjs/core': '>=6.7.0'
ioredis: '>=5.0.0'
'@nestjs/axios@4.0.1':
resolution: {integrity: sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==}
peerDependencies:
'@nestjs/common': ^10.0.0 || ^11.0.0
axios: ^1.3.1
rxjs: ^7.0.0
'@nestjs/bull-shared@11.0.4':
resolution: {integrity: sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==}
peerDependencies:
@@ -1124,6 +1175,102 @@ packages:
class-validator:
optional: true
'@nestjs/terminus@10.2.0':
resolution: {integrity: sha512-zPs98xvJ4ogEimRQOz8eU90mb7z+W/kd/mL4peOgrJ/VqER+ibN2Cboj65uJZW3XuNhpOqaeYOJte86InJd44A==}
peerDependencies:
'@grpc/grpc-js': '*'
'@grpc/proto-loader': '*'
'@mikro-orm/core': '*'
'@mikro-orm/nestjs': '*'
'@nestjs/axios': ^1.0.0 || ^2.0.0 || ^3.0.0
'@nestjs/common': ^9.0.0 || ^10.0.0
'@nestjs/core': ^9.0.0 || ^10.0.0
'@nestjs/microservices': ^9.0.0 || ^10.0.0
'@nestjs/mongoose': ^9.0.0 || ^10.0.0
'@nestjs/sequelize': ^9.0.0 || ^10.0.0
'@nestjs/typeorm': ^9.0.0 || ^10.0.0
'@prisma/client': '*'
mongoose: '*'
reflect-metadata: 0.1.x
rxjs: 7.x
sequelize: '*'
typeorm: '*'
peerDependenciesMeta:
'@grpc/grpc-js':
optional: true
'@grpc/proto-loader':
optional: true
'@mikro-orm/core':
optional: true
'@mikro-orm/nestjs':
optional: true
'@nestjs/axios':
optional: true
'@nestjs/microservices':
optional: true
'@nestjs/mongoose':
optional: true
'@nestjs/sequelize':
optional: true
'@nestjs/typeorm':
optional: true
'@prisma/client':
optional: true
mongoose:
optional: true
sequelize:
optional: true
typeorm:
optional: true
'@nestjs/terminus@11.0.0':
resolution: {integrity: sha512-c55LOo9YGovmQHtFUMa/vDaxGZ2cglMTZejqgHREaApt/GArTfgYYGwhRXPLq8ZwiQQlLuYB+79e9iA8mlDSLA==}
peerDependencies:
'@grpc/grpc-js': '*'
'@grpc/proto-loader': '*'
'@mikro-orm/core': '*'
'@mikro-orm/nestjs': '*'
'@nestjs/axios': ^2.0.0 || ^3.0.0 || ^4.0.0
'@nestjs/common': ^10.0.0 || ^11.0.0
'@nestjs/core': ^10.0.0 || ^11.0.0
'@nestjs/microservices': ^10.0.0 || ^11.0.0
'@nestjs/mongoose': ^11.0.0
'@nestjs/sequelize': ^10.0.0 || ^11.0.0
'@nestjs/typeorm': ^10.0.0 || ^11.0.0
'@prisma/client': '*'
mongoose: '*'
reflect-metadata: 0.1.x || 0.2.x
rxjs: 7.x
sequelize: '*'
typeorm: '*'
peerDependenciesMeta:
'@grpc/grpc-js':
optional: true
'@grpc/proto-loader':
optional: true
'@mikro-orm/core':
optional: true
'@mikro-orm/nestjs':
optional: true
'@nestjs/axios':
optional: true
'@nestjs/microservices':
optional: true
'@nestjs/mongoose':
optional: true
'@nestjs/sequelize':
optional: true
'@nestjs/typeorm':
optional: true
'@prisma/client':
optional: true
mongoose:
optional: true
sequelize:
optional: true
typeorm:
optional: true
'@nestjs/testing@11.1.9':
resolution: {integrity: sha512-UFxerBDdb0RUNxQNj25pvkvNE7/vxKhXYWBt3QuwBFnYISzRIzhVlyIqLfoV5YI3zV0m0Nn4QAn1KM0zzwfEng==}
peerDependencies:
@@ -1424,6 +1571,9 @@ packages:
resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==}
engines: {node: '>=18.0.0'}
'@so-ric/colorspace@1.1.6':
resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==}
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
@@ -1458,6 +1608,9 @@ packages:
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/async-retry@1.4.9':
resolution: {integrity: sha512-s1ciZQJzRh3708X/m3vPExr5KJlzlZJvXsKpbtE2luqNcbROr64qU+3KpJsYHqWMeaxI839OvXf9PrUSw1Xtyg==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@@ -1565,6 +1718,9 @@ packages:
'@types/nodemailer@7.0.4':
resolution: {integrity: sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==}
'@types/opossum@8.1.9':
resolution: {integrity: sha512-Jm/tYxuJFefiwRYs+/EOsUP3ktk0c8siMgAHPLnA4PXF4wKghzcjqf88dY+Xii5jId5Txw4JV0FMKTpjbd7KJA==}
'@types/passport-jwt@4.0.1':
resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==}
@@ -1580,6 +1736,9 @@ packages:
'@types/range-parser@1.2.7':
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
'@types/retry@0.12.5':
resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==}
'@types/send@0.17.6':
resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==}
@@ -1598,6 +1757,9 @@ packages:
'@types/supertest@6.0.3':
resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==}
'@types/triple-beam@1.3.5':
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
'@types/uuid@11.0.0':
resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==}
deprecated: This is a stub types definition. uuid provides its own type definitions, so you do not need this installed.
@@ -1891,6 +2053,9 @@ packages:
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
@@ -1961,6 +2126,12 @@ packages:
asap@2.0.6:
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
async-retry@1.3.3:
resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -2018,6 +2189,9 @@ packages:
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
engines: {node: '>= 18'}
bintrees@1.0.2:
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
@@ -2028,6 +2202,10 @@ packages:
bowser@2.12.1:
resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==}
boxen@5.1.2:
resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==}
engines: {node: '>=10'}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
@@ -2127,6 +2305,10 @@ packages:
chardet@2.1.1:
resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
check-disk-space@3.4.0:
resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==}
engines: {node: '>=16'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
@@ -2148,6 +2330,10 @@ packages:
class-validator@0.14.2:
resolution: {integrity: sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==}
cli-boxes@2.2.1:
resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==}
engines: {node: '>=6'}
cli-cursor@3.1.0:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
@@ -2187,9 +2373,25 @@ packages:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-convert@3.1.3:
resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==}
engines: {node: '>=14.6'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-name@2.1.0:
resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==}
engines: {node: '>=12.20'}
color-string@2.1.4:
resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==}
engines: {node: '>=18'}
color@5.0.3:
resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==}
engines: {node: '>=18'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@@ -2393,6 +2595,9 @@ packages:
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
enabled@2.0.0:
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
encodeurl@2.0.0:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
@@ -2580,6 +2785,9 @@ packages:
fb-watchman@2.0.2:
resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
fecha@4.2.3:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
@@ -2626,6 +2834,9 @@ packages:
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
@@ -3142,6 +3353,9 @@ packages:
keyv@5.5.4:
resolution: {integrity: sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==}
kuler@2.0.0:
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
engines: {node: '>=6'}
@@ -3218,6 +3432,10 @@ packages:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
logform@2.7.0:
resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==}
engines: {node: '>= 12.0.0'}
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
@@ -3383,6 +3601,12 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
nest-winston@1.10.2:
resolution: {integrity: sha512-Z9IzL/nekBOF/TEwBHUJDiDPMaXUcFquUQOFavIRet6xF0EbuWnOzslyN/ksgzG+fITNgXhMdrL/POp9SdaFxA==}
peerDependencies:
'@nestjs/common': ^5.0.0 || ^6.6.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
winston: ^3.0.0
node-abort-controller@3.1.1:
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
@@ -3438,10 +3662,17 @@ packages:
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
one-time@1.0.0:
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
onetime@5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
opossum@9.0.0:
resolution: {integrity: sha512-K76U0QkxOfUZamneQuzz+AP0fyfTJcCplZ2oZL93nxeupuJbN4s6uFNbmVCt4eWqqGqRnnowdFuBicJ1fLMVxw==}
engines: {node: ^24 || ^22 || ^20}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -3574,6 +3805,10 @@ packages:
resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
prom-client@15.1.3:
resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==}
engines: {node: ^16 || ^18 || >=20}
promise-coalesce@1.5.0:
resolution: {integrity: sha512-cTJ30U+ur1LD7pMPyQxiKIwxjtAjLsyU7ivRhVWZrX9BNIXtf78pc37vSMc8Vikx7DVzEKNk2SEJ5KWUpSG2ig==}
engines: {node: '>=16'}
@@ -3663,6 +3898,10 @@ packages:
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
engines: {node: '>=8'}
retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'}
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -3683,6 +3922,10 @@ packages:
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-stable-stringify@2.5.0:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@@ -3807,6 +4050,9 @@ packages:
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
engines: {node: '>= 0.6'}
stack-trace@0.0.10:
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
stack-utils@2.0.6:
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'}
@@ -3913,6 +4159,9 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
tdigest@0.1.2:
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
terser-webpack-plugin@5.3.14:
resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==}
engines: {node: '>= 10.13.0'}
@@ -3938,6 +4187,9 @@ packages:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
engines: {node: '>=8'}
text-hex@1.0.0:
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
tmpl@1.0.5:
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
@@ -3957,6 +4209,10 @@ packages:
resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
engines: {node: '>=14.16'}
triple-beam@1.4.1:
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
engines: {node: '>= 14.0.0'}
ts-api-utils@2.1.0:
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
engines: {node: '>=18.12'}
@@ -4030,6 +4286,10 @@ packages:
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
engines: {node: '>=4'}
type-fest@0.20.2:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
type-fest@0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
@@ -4235,6 +4495,18 @@ packages:
engines: {node: '>= 8'}
hasBin: true
widest-line@3.1.0:
resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==}
engines: {node: '>=8'}
winston-transport@4.9.0:
resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==}
engines: {node: '>= 12.0.0'}
winston@3.18.3:
resolution: {integrity: sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==}
engines: {node: '>= 12.0.0'}
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@@ -4968,10 +5240,18 @@ snapshots:
'@colors/colors@1.5.0':
optional: true
'@colors/colors@1.6.0': {}
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@dabh/diagnostics@2.0.8':
dependencies:
'@so-ric/colorspace': 1.1.6
enabled: 2.0.0
kuler: 2.0.0
'@elastic/elasticsearch@8.19.1':
dependencies:
'@elastic/transport': 8.10.0
@@ -5489,6 +5769,36 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
'@nestjs-modules/ioredis@2.0.2(@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))))(ioredis@5.8.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
ioredis: 5.8.2
optionalDependencies:
'@nestjs/terminus': 10.2.0(@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
transitivePeerDependencies:
- '@grpc/grpc-js'
- '@grpc/proto-loader'
- '@mikro-orm/core'
- '@mikro-orm/nestjs'
- '@nestjs/axios'
- '@nestjs/microservices'
- '@nestjs/mongoose'
- '@nestjs/sequelize'
- '@nestjs/typeorm'
- '@prisma/client'
- mongoose
- reflect-metadata
- rxjs
- sequelize
- typeorm
'@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
axios: 1.13.2
rxjs: 7.8.2
'@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -5656,6 +5966,33 @@ snapshots:
class-transformer: 0.5.1
class-validator: 0.14.2
'@nestjs/terminus@10.2.0(@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
boxen: 5.1.2
check-disk-space: 3.4.0
reflect-metadata: 0.2.2
rxjs: 7.8.2
optionalDependencies:
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)
'@nestjs/typeorm': 11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
typeorm: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
optional: true
'@nestjs/terminus@11.0.0(@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
boxen: 5.1.2
check-disk-space: 3.4.0
reflect-metadata: 0.2.2
rxjs: 7.8.2
optionalDependencies:
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)
'@nestjs/typeorm': 11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
typeorm: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
'@nestjs/testing@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -6038,6 +6375,11 @@ snapshots:
dependencies:
tslib: 2.8.1
'@so-ric/colorspace@1.1.6':
dependencies:
color: 5.0.3
text-hex: 1.0.0
'@socket.io/component-emitter@3.1.2': {}
'@sqltools/formatter@1.2.5': {}
@@ -6071,6 +6413,10 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/async-retry@1.4.9':
dependencies:
'@types/retry': 0.12.5
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.28.5
@@ -6210,6 +6556,10 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@types/opossum@8.1.9':
dependencies:
'@types/node': 22.19.1
'@types/passport-jwt@4.0.1':
dependencies:
'@types/jsonwebtoken': 9.0.10
@@ -6228,6 +6578,8 @@ snapshots:
'@types/range-parser@1.2.7': {}
'@types/retry@0.12.5': {}
'@types/send@0.17.6':
dependencies:
'@types/mime': 1.3.5
@@ -6257,6 +6609,8 @@ snapshots:
'@types/methods': 1.1.4
'@types/superagent': 8.1.9
'@types/triple-beam@1.3.5': {}
'@types/uuid@11.0.0':
dependencies:
uuid: 13.0.0
@@ -6574,6 +6928,10 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ansi-align@3.0.1:
dependencies:
string-width: 4.2.3
ansi-colors@4.1.3: {}
ansi-escapes@4.3.2:
@@ -6633,6 +6991,12 @@ snapshots:
asap@2.0.6: {}
async-retry@1.3.3:
dependencies:
retry: 0.13.1
async@3.2.6: {}
asynckit@0.4.0: {}
available-typed-arrays@1.0.7:
@@ -6714,6 +7078,8 @@ snapshots:
node-addon-api: 8.5.0
node-gyp-build: 4.8.4
bintrees@1.0.2: {}
bl@4.1.0:
dependencies:
buffer: 5.7.1
@@ -6736,6 +7102,17 @@ snapshots:
bowser@2.12.1: {}
boxen@5.1.2:
dependencies:
ansi-align: 3.0.1
camelcase: 6.3.0
chalk: 4.1.2
cli-boxes: 2.2.1
string-width: 4.2.3
type-fest: 0.20.2
widest-line: 3.1.0
wrap-ansi: 7.0.0
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
@@ -6858,6 +7235,8 @@ snapshots:
chardet@2.1.1: {}
check-disk-space@3.4.0: {}
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
@@ -6876,6 +7255,8 @@ snapshots:
libphonenumber-js: 1.12.27
validator: 13.15.23
cli-boxes@2.2.1: {}
cli-cursor@3.1.0:
dependencies:
restore-cursor: 3.1.0
@@ -6908,8 +7289,23 @@ snapshots:
dependencies:
color-name: 1.1.4
color-convert@3.1.3:
dependencies:
color-name: 2.1.0
color-name@1.1.4: {}
color-name@2.1.0: {}
color-string@2.1.4:
dependencies:
color-name: 2.1.0
color@5.0.3:
dependencies:
color-convert: 3.1.3
color-string: 2.1.4
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
@@ -7070,6 +7466,8 @@ snapshots:
emoji-regex@9.2.2: {}
enabled@2.0.0: {}
encodeurl@2.0.0: {}
engine.io-parser@5.2.3: {}
@@ -7306,6 +7704,8 @@ snapshots:
dependencies:
bser: 2.1.1
fecha@4.2.3: {}
fflate@0.8.2: {}
file-entry-cache@8.0.0:
@@ -7357,6 +7757,8 @@ snapshots:
flatted@3.3.3: {}
fn.name@1.1.0: {}
follow-redirects@1.15.11: {}
for-each@0.3.5:
@@ -8071,6 +8473,8 @@ snapshots:
dependencies:
'@keyv/serialize': 1.1.1
kuler@2.0.0: {}
leven@3.1.0: {}
levn@0.4.1:
@@ -8127,6 +8531,15 @@ snapshots:
chalk: 4.1.2
is-unicode-supported: 0.1.0
logform@2.7.0:
dependencies:
'@colors/colors': 1.6.0
'@types/triple-beam': 1.3.5
fecha: 4.2.3
ms: 2.1.3
safe-stable-stringify: 2.5.0
triple-beam: 1.4.1
long@5.3.2: {}
lru-cache@10.4.3: {}
@@ -8272,6 +8685,12 @@ snapshots:
neo-async@2.6.2: {}
nest-winston@1.10.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(winston@3.18.3):
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
fast-safe-stringify: 2.1.1
winston: 3.18.3
node-abort-controller@3.1.1: {}
node-addon-api@8.5.0: {}
@@ -8313,10 +8732,16 @@ snapshots:
dependencies:
wrappy: 1.0.2
one-time@1.0.0:
dependencies:
fn.name: 1.1.0
onetime@5.1.2:
dependencies:
mimic-fn: 2.1.0
opossum@9.0.0: {}
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -8438,6 +8863,11 @@ snapshots:
ansi-styles: 5.2.0
react-is: 18.3.1
prom-client@15.1.3:
dependencies:
'@opentelemetry/api': 1.9.0
tdigest: 0.1.2
promise-coalesce@1.5.0: {}
proxy-addr@2.0.7:
@@ -8518,6 +8948,8 @@ snapshots:
onetime: 5.1.2
signal-exit: 3.0.7
retry@0.13.1: {}
reusify@1.1.0: {}
router@2.2.0:
@@ -8544,6 +8976,8 @@ snapshots:
safe-buffer@5.2.1: {}
safe-stable-stringify@2.5.0: {}
safer-buffer@2.1.2: {}
schema-utils@3.3.0:
@@ -8705,6 +9139,8 @@ snapshots:
sqlstring@2.3.3: {}
stack-trace@0.0.10: {}
stack-utils@2.0.6:
dependencies:
escape-string-regexp: 2.0.0
@@ -8811,6 +9247,10 @@ snapshots:
tapable@2.3.0: {}
tdigest@0.1.2:
dependencies:
bintrees: 1.0.2
terser-webpack-plugin@5.3.14(webpack@5.100.2):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
@@ -8833,6 +9273,8 @@ snapshots:
glob: 7.2.3
minimatch: 3.1.2
text-hex@1.0.0: {}
tmpl@1.0.5: {}
to-buffer@1.2.2:
@@ -8853,6 +9295,8 @@ snapshots:
'@tokenizer/token': 0.3.0
ieee754: 1.2.1
triple-beam@1.4.1: {}
ts-api-utils@2.1.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
@@ -8926,6 +9370,8 @@ snapshots:
type-detect@4.0.8: {}
type-fest@0.20.2: {}
type-fest@0.21.3: {}
type-fest@4.41.0: {}
@@ -9126,6 +9572,30 @@ snapshots:
dependencies:
isexe: 2.0.0
widest-line@3.1.0:
dependencies:
string-width: 4.2.3
winston-transport@4.9.0:
dependencies:
logform: 2.7.0
readable-stream: 3.6.2
triple-beam: 1.4.1
winston@3.18.3:
dependencies:
'@colors/colors': 1.6.0
'@dabh/diagnostics': 2.0.8
async: 3.2.6
is-stream: 2.0.1
logform: 2.7.0
one-time: 1.0.0
readable-stream: 3.6.2
safe-stable-stringify: 2.5.0
stack-trace: 0.0.10
triple-beam: 1.4.1
winston-transport: 4.9.0
word-wrap@1.2.5: {}
wordwrap@1.0.0: {}

View File

@@ -42,6 +42,7 @@ import { MonitoringModule } from './modules/monitoring/monitoring.module';
import { ResilienceModule } from './common/resilience/resilience.module'; // ✅ Import
// ... imports
import { SearchModule } from './modules/search/search.module'; // ✅ Import
import { RedisModule } from '@nestjs-modules/ioredis'; // [NEW]
@Module({
imports: [
// 1. Setup Config Module พร้อม Validation
@@ -113,7 +114,18 @@ import { SearchModule } from './modules/search/search.module'; // ✅ Import
},
}),
}),
// [NEW] Setup Redis Module (สำหรับ InjectRedis)
RedisModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'single',
url: `redis://${configService.get('REDIS_HOST')}:${configService.get('REDIS_PORT')}`,
options: {
password: configService.get('REDIS_PASSWORD'),
},
}),
inject: [ConfigService],
}),
// 📊 Register Monitoring Module (Health & Metrics) [Req 6.10]
MonitoringModule,

View File

@@ -6,6 +6,7 @@ import {
CreateDateColumn,
ManyToOne,
JoinColumn,
PrimaryColumn, // ✅ [Fix] เพิ่ม Import นี้
} from 'typeorm';
import { User } from '../../modules/user/entities/user.entity';
@@ -46,7 +47,9 @@ export class AuditLog {
@Column({ name: 'user_agent', length: 255, nullable: true })
userAgent?: string;
// ✅ [Fix] รวม Decorator ไว้ที่นี่ที่เดียว
@CreateDateColumn({ name: 'created_at' })
@PrimaryColumn() // เพื่อบอกว่าเป็น Composite PK คู่กับ auditId
createdAt!: Date;
// Relations

View File

@@ -0,0 +1,75 @@
-- ============================================================
-- Database Partitioning Script for LCBP3-DMS (Fixed #1075)
-- Target Tables: audit_logs, notifications
-- Strategy: Range Partitioning by YEAR(created_at)
-- ============================================================
-- ------------------------------------------------------------
-- 1. Audit Logs Partitioning
-- ------------------------------------------------------------
-- Step 1: เอา AUTO_INCREMENT ออกก่อน (เพื่อไม่ให้ติด Error 1075 ตอนลบ PK)
ALTER TABLE audit_logs
MODIFY audit_id BIGINT NOT NULL;
-- Step 2: ลบ Primary Key เดิม
ALTER TABLE audit_logs DROP PRIMARY KEY;
-- Step 3: สร้าง Primary Key ใหม่ (รวม created_at เพื่อทำ Partition)
ALTER TABLE audit_logs
ADD PRIMARY KEY (audit_id, created_at);
-- Step 4: ใส่ AUTO_INCREMENT กลับเข้าไป
ALTER TABLE audit_logs
MODIFY audit_id BIGINT NOT NULL AUTO_INCREMENT;
-- Step 5: สร้าง Partition
ALTER TABLE audit_logs PARTITION BY RANGE (YEAR(created_at)) (
PARTITION p_old
VALUES LESS THAN (2024),
PARTITION p2024
VALUES LESS THAN (2025),
PARTITION p2025
VALUES LESS THAN (2026),
PARTITION p2026
VALUES LESS THAN (2027),
PARTITION p2027
VALUES LESS THAN (2028),
PARTITION p2028
VALUES LESS THAN (2029),
PARTITION p2029
VALUES LESS THAN (2030),
PARTITION p2030
VALUES LESS THAN (2031),
PARTITION p_future
VALUES LESS THAN MAXVALUE
);
-- ------------------------------------------------------------
-- 2. Notifications Partitioning
-- ------------------------------------------------------------
-- Step 1: เอา AUTO_INCREMENT ออกก่อน
ALTER TABLE notifications
MODIFY id INT NOT NULL;
-- Step 2: ลบ Primary Key เดิม
ALTER TABLE notifications DROP PRIMARY KEY;
-- Step 3: สร้าง Primary Key ใหม่
ALTER TABLE notifications
ADD PRIMARY KEY (id, created_at);
-- Step 4: ใส่ AUTO_INCREMENT กลับเข้าไป
ALTER TABLE notifications
MODIFY id INT NOT NULL AUTO_INCREMENT;
-- Step 5: สร้าง Partition
ALTER TABLE notifications PARTITION BY RANGE (YEAR(created_at)) (
PARTITION p_old
VALUES LESS THAN (2024),
PARTITION p2024
VALUES LESS THAN (2025),
PARTITION p2025
VALUES LESS THAN (2026),
PARTITION p2026
VALUES LESS THAN (2027),
PARTITION p2027
VALUES LESS THAN (2028),
PARTITION p2028
VALUES LESS THAN (2029),
PARTITION p2029
VALUES LESS THAN (2030),
PARTITION p2030
VALUES LESS THAN (2031),
PARTITION p_future
VALUES LESS THAN MAXVALUE
);

View File

@@ -0,0 +1,82 @@
// src/database/seeds/workflow-definitions.seed.ts
import { DataSource } from 'typeorm';
import { WorkflowDefinition } from '../../modules/workflow-engine/entities/workflow-definition.entity';
import { WorkflowDslService } from '../../modules/workflow-engine/workflow-dsl.service';
export const seedWorkflowDefinitions = async (dataSource: DataSource) => {
const repo = dataSource.getRepository(WorkflowDefinition);
const dslService = new WorkflowDslService();
// 1. RFA Workflow (Standard)
const rfaDsl = {
workflow: 'RFA',
version: 1,
states: [
{
name: 'DRAFT',
initial: true,
on: { SUBMIT: { to: 'IN_REVIEW', requirements: [{ role: 'Editor' }] } },
},
{
name: 'IN_REVIEW',
on: {
APPROVE: {
to: 'APPROVED',
requirements: [{ role: 'Contract Admin' }],
},
REJECT: {
to: 'REJECTED',
requirements: [{ role: 'Contract Admin' }],
},
COMMENT: { to: 'DRAFT', requirements: [{ role: 'Contract Admin' }] }, // ส่งกลับแก้ไข
},
},
{ name: 'APPROVED', terminal: true },
{ name: 'REJECTED', terminal: true },
],
};
// 2. Circulation Workflow
const circulationDsl = {
workflow: 'CIRCULATION',
version: 1,
states: [
{
name: 'OPEN',
initial: true,
on: { SEND: { to: 'IN_REVIEW' } },
},
{
name: 'IN_REVIEW',
on: {
COMPLETE: { to: 'COMPLETED' }, // เมื่อทุกคนตอบครบ
CANCEL: { to: 'CANCELLED' },
},
},
{ name: 'COMPLETED', terminal: true },
{ name: 'CANCELLED', terminal: true },
],
};
const workflows = [rfaDsl, circulationDsl];
for (const dsl of workflows) {
const exists = await repo.findOne({
where: { workflow_code: dsl.workflow, version: dsl.version },
});
if (!exists) {
const compiled = dslService.compile(dsl);
await repo.save(
repo.create({
workflow_code: dsl.workflow,
version: dsl.version,
dsl: dsl,
compiled: compiled,
is_active: true,
}),
);
console.log(`✅ Seeded Workflow: ${dsl.workflow} v${dsl.version}`);
}
}
};

View File

@@ -1,5 +1,3 @@
// File: src/modules/master/dto/create-tag.dto.ts
import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
@@ -7,12 +5,9 @@ export class CreateTagDto {
@ApiProperty({ example: 'URGENT', description: 'ชื่อ Tag' })
@IsString()
@IsNotEmpty()
tag_name: string;
tag_name!: string; // เพิ่ม !
@ApiProperty({
example: 'เอกสารด่วนต้องดำเนินการทันที',
description: 'คำอธิบาย',
})
@ApiProperty({ example: 'คำอธิบาย', description: 'คำอธิบาย' })
@IsString()
@IsOptional()
description?: string;

View File

@@ -1,5 +1,3 @@
// File: src/modules/master/entities/tag.entity.ts
import {
Entity,
Column,
@@ -11,17 +9,17 @@ import {
@Entity('tags')
export class Tag {
@PrimaryGeneratedColumn()
id: number;
id!: number; // เพิ่ม !
@Column({ length: 100, unique: true, comment: 'ชื่อ Tag' })
tag_name: string;
@Column({ length: 100, unique: true })
tag_name!: string; // เพิ่ม !
@Column({ type: 'text', nullable: true, comment: 'คำอธิบายแท็ก' })
description: string;
@Column({ type: 'text', nullable: true })
description!: string; // เพิ่ม !
@CreateDateColumn()
created_at: Date;
created_at!: Date; // เพิ่ม !
@UpdateDateColumn()
updated_at: Date;
updated_at!: Date; // เพิ่ม !
}

View File

@@ -49,15 +49,15 @@ export class MasterService {
async findAllCorrespondenceTypes() {
return this.corrTypeRepo.find({
where: { is_active: true },
order: { sort_order: 'ASC' },
where: { isActive: true }, // ✅ แก้เป็น camelCase
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
});
}
async findAllCorrespondenceStatuses() {
return this.corrStatusRepo.find({
where: { is_active: true },
order: { sort_order: 'ASC' },
where: { isActive: true }, // ✅ แก้เป็น camelCase
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
});
}
@@ -67,22 +67,22 @@ export class MasterService {
async findAllRfaTypes() {
return this.rfaTypeRepo.find({
where: { is_active: true },
order: { sort_order: 'ASC' },
where: { isActive: true }, // ✅ แก้เป็น camelCase
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
});
}
async findAllRfaStatuses() {
return this.rfaStatusRepo.find({
where: { is_active: true },
order: { sort_order: 'ASC' },
where: { isActive: true }, // ✅ แก้เป็น camelCase
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
});
}
async findAllRfaApproveCodes() {
return this.rfaApproveRepo.find({
where: { is_active: true },
order: { sort_order: 'ASC' },
where: { isActive: true }, // ✅ แก้เป็น camelCase
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
});
}
@@ -92,8 +92,8 @@ export class MasterService {
async findAllCirculationStatuses() {
return this.circulationStatusRepo.find({
where: { is_active: true },
order: { sort_order: 'ASC' },
where: { isActive: true }, // ✅ แก้เป็น camelCase
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
});
}
@@ -101,9 +101,6 @@ export class MasterService {
// 🏷️ Tag Management (CRUD)
// =================================================================
/**
* ค้นหา Tag ทั้งหมด พร้อมรองรับการ Search และ Pagination
*/
async findAllTags(query?: SearchTagDto) {
const qb = this.tagRepo.createQueryBuilder('tag');
@@ -115,14 +112,12 @@ export class MasterService {
qb.orderBy('tag.tag_name', 'ASC');
// Pagination Logic
if (query?.page && query?.limit) {
const page = query.page;
const limit = query.limit;
qb.skip((page - 1) * limit).take(limit);
}
// ถ้ามีการแบ่งหน้า ให้ส่งคืนทั้งข้อมูลและจำนวนทั้งหมด (count)
if (query?.page && query?.limit) {
const [items, total] = await qb.getManyAndCount();
return {
@@ -153,7 +148,7 @@ export class MasterService {
}
async updateTag(id: number, dto: UpdateTagDto) {
const tag = await this.findOneTag(id); // Reuse findOne for check
const tag = await this.findOneTag(id);
Object.assign(tag, dto);
return this.tagRepo.save(tag);
}

View File

@@ -0,0 +1,16 @@
import { IsBoolean, IsOptional, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class SetMaintenanceDto {
@ApiProperty({ description: 'สถานะ Maintenance (true = เปิด, false = ปิด)' })
@IsBoolean()
enabled!: boolean; // ✅ เพิ่ม ! ตรงนี้
@ApiProperty({
description: 'เหตุผลที่ปิดปรับปรุง (แสดงให้ User เห็น)',
required: false,
})
@IsOptional()
@IsString()
reason?: string; // Optional (?) ไม่ต้องใส่ !
}

View File

@@ -0,0 +1,30 @@
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { MonitoringService } from './monitoring.service';
import { SetMaintenanceDto } from './dto/set-maintenance.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { BypassMaintenance } from '../../common/decorators/bypass-maintenance.decorator';
@ApiTags('System Monitoring')
@Controller('monitoring')
export class MonitoringController {
constructor(private readonly monitoringService: MonitoringService) {}
@Get('maintenance')
@ApiOperation({ summary: 'Check maintenance status (Public)' })
@BypassMaintenance() // API นี้ต้องเรียกได้แม้ระบบปิดอยู่
getMaintenanceStatus() {
return this.monitoringService.getMaintenanceStatus();
}
@Post('maintenance')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@RequirePermission('system.manage_all') // เฉพาะ Superadmin เท่านั้น
@BypassMaintenance() // Admin ต้องยิงเปิด/ปิดได้แม้ระบบจะปิดอยู่
@ApiOperation({ summary: 'Toggle Maintenance Mode (Admin Only)' })
setMaintenanceMode(@Body() dto: SetMaintenanceDto) {
return this.monitoringService.setMaintenanceMode(dto);
}
}

View File

@@ -1,23 +1,34 @@
// File: src/modules/monitoring/monitoring.module.ts
import { Global, Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HttpModule } from '@nestjs/axios';
import { APP_INTERCEPTOR } from '@nestjs/core';
// Existing Components
import { HealthController } from './controllers/health.controller';
import { MetricsService } from './services/metrics.service';
import { PerformanceInterceptor } from '../../common/interceptors/performance.interceptor';
@Global() // ทำให้ Module นี้ใช้งานได้ทั่วทั้ง App โดยไม่ต้อง Import ซ้ำ
// [NEW] Maintenance Mode Components
import { MonitoringController } from './monitoring.controller';
import { MonitoringService } from './monitoring.service';
@Global() // Module นี้เป็น Global (ดีแล้วครับ)
@Module({
imports: [TerminusModule, HttpModule],
controllers: [HealthController],
controllers: [
HealthController, // ✅ ของเดิม: /health
MonitoringController, // ✅ ของใหม่: /monitoring/maintenance
],
providers: [
MetricsService,
MetricsService, // ✅ ของเดิม
MonitoringService, // ✅ ของใหม่ (Logic เปิด/ปิด Maintenance)
{
provide: APP_INTERCEPTOR, // Register Global Interceptor
useClass: PerformanceInterceptor,
provide: APP_INTERCEPTOR,
useClass: PerformanceInterceptor, // ✅ ของเดิม (จับเวลา Response Time)
},
],
exports: [MetricsService],
exports: [MetricsService, MonitoringService],
})
export class MonitoringModule {}

View File

@@ -0,0 +1,44 @@
// File: src/modules/monitoring/monitoring.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';
import { SetMaintenanceDto } from './dto/set-maintenance.dto';
@Injectable()
export class MonitoringService {
private readonly logger = new Logger(MonitoringService.name);
private readonly MAINTENANCE_KEY = 'system:maintenance_mode';
constructor(@InjectRedis() private readonly redis: Redis) {}
/**
* ตรวจสอบสถานะปัจจุบัน
*/
async getMaintenanceStatus() {
const status = await this.redis.get(this.MAINTENANCE_KEY);
return {
isEnabled: status === 'true',
message:
status === 'true' ? 'System is under maintenance' : 'System is normal',
};
}
/**
* ตั้งค่า Maintenance Mode
*/
async setMaintenanceMode(dto: SetMaintenanceDto) {
if (dto.enabled) {
await this.redis.set(this.MAINTENANCE_KEY, 'true');
// เก็บเหตุผลไว้ใน Key อื่นก็ได้ถ้าต้องการ แต่เบื้องต้น Guard เช็คแค่ Key นี้
this.logger.warn(
`⚠️ SYSTEM ENTERED MAINTENANCE MODE: ${dto.reason || 'No reason provided'}`,
);
} else {
await this.redis.del(this.MAINTENANCE_KEY);
this.logger.log('✅ System exited maintenance mode');
}
return this.getMaintenanceStatus();
}
}

View File

@@ -5,6 +5,7 @@ import {
CreateDateColumn,
ManyToOne,
JoinColumn,
PrimaryColumn, // ✅ [Fix] เพิ่ม Import นี้
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
@@ -44,7 +45,9 @@ export class Notification {
@Column({ name: 'entity_id', nullable: true })
entityId?: number;
// ✅ [Fix] รวม Decorator ไว้ที่นี่ที่เดียว (เป็นทั้ง CreateDate และ PrimaryColumn สำหรับ Partition)
@CreateDateColumn({ name: 'created_at' })
@PrimaryColumn()
createdAt!: Date;
// --- Relations ---

View File

@@ -1,26 +1,43 @@
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq';
// File: src/modules/notification/notification.processor.ts
import { Processor, WorkerHost, InjectQueue } from '@nestjs/bullmq';
import { Job, Queue } from 'bullmq';
import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';
import * as nodemailer from 'nodemailer';
import axios from 'axios';
import { UserService } from '../user/user.service';
interface NotificationPayload {
userId: number;
title: string;
message: string;
link: string;
type: 'EMAIL' | 'LINE' | 'SYSTEM';
}
@Processor('notifications')
export class NotificationProcessor extends WorkerHost {
private readonly logger = new Logger(NotificationProcessor.name);
private mailerTransport: nodemailer.Transporter;
// ค่าคงที่สำหรับ Digest (เช่น รอ 5 นาที)
private readonly DIGEST_DELAY = 5 * 60 * 1000;
constructor(
private configService: ConfigService,
private userService: UserService,
@InjectQueue('notifications') private notificationQueue: Queue,
@InjectRedis() private readonly redis: Redis,
) {
super();
// Setup Nodemailer
this.mailerTransport = nodemailer.createTransport({
host: this.configService.get('SMTP_HOST'),
port: this.configService.get('SMTP_PORT'),
port: Number(this.configService.get('SMTP_PORT')),
secure: this.configService.get('SMTP_SECURE') === 'true',
auth: {
user: this.configService.get('SMTP_USER'),
@@ -30,59 +47,196 @@ export class NotificationProcessor extends WorkerHost {
}
async process(job: Job<any, any, string>): Promise<any> {
this.logger.debug(`Processing job ${job.name} for user ${job.data.userId}`);
this.logger.debug(`Processing job ${job.name} (ID: ${job.id})`);
switch (job.name) {
case 'send-email':
return this.handleSendEmail(job.data);
case 'send-line':
return this.handleSendLine(job.data);
default:
throw new Error(`Unknown job name: ${job.name}`);
try {
switch (job.name) {
case 'dispatch-notification':
// Job หลัก: ตัดสินใจว่าจะส่งเลย หรือจะเข้า Digest Queue
return this.handleDispatch(job.data);
case 'process-digest':
// Job รอง: ทำงานเมื่อครบเวลา Delay เพื่อส่งแบบรวม
return this.handleProcessDigest(job.data.userId, job.data.type);
default:
throw new Error(`Unknown job name: ${job.name}`);
}
} catch (error) {
// ✅ แก้ไขตรงนี้: Type Casting (error as Error)
this.logger.error(
`Failed to process job ${job.name}: ${(error as Error).message}`,
(error as Error).stack,
);
throw error; // ให้ BullMQ จัดการ Retry
}
}
private async handleSendEmail(data: any) {
const user = await this.userService.findOne(data.userId);
if (!user || !user.email) {
this.logger.warn(`User ${data.userId} has no email`);
/**
* ฟังก์ชันตัดสินใจ (Dispatcher)
* ตรวจสอบ User Preferences และ Digest Mode
*/
private async handleDispatch(data: NotificationPayload) {
// 1. ดึง User พร้อม Preferences
const user: any = await this.userService.findOne(data.userId);
if (!user) {
this.logger.warn(`User ${data.userId} not found, skipping notification.`);
return;
}
const prefs = user.preferences || {
notify_email: true,
notify_line: true,
digest_mode: false,
};
// 2. ตรวจสอบว่า User ปิดรับการแจ้งเตือนหรือไม่
if (data.type === 'EMAIL' && !prefs.notify_email) return;
if (data.type === 'LINE' && !prefs.notify_line) return;
// 3. ตรวจสอบ Digest Mode
if (prefs.digest_mode) {
await this.addToDigest(data);
} else {
// ส่งทันที (Real-time)
if (data.type === 'EMAIL') await this.sendEmailImmediate(user, data);
if (data.type === 'LINE') await this.sendLineImmediate(user, data);
}
}
/**
* เพิ่มข้อความลงใน Redis List และตั้งเวลาส่ง (Delayed Job)
*/
private async addToDigest(data: NotificationPayload) {
const key = `digest:${data.type}:${data.userId}`;
// 1. Push ข้อมูลลง Redis List
await this.redis.rpush(key, JSON.stringify(data));
// 2. ตรวจสอบว่ามี "ตัวนับเวลาถอยหลัง" (Delayed Job) อยู่หรือยัง?
const lockKey = `digest:lock:${data.type}:${data.userId}`;
const isLocked = await this.redis.get(lockKey);
if (!isLocked) {
// ถ้ายังไม่มี Job รออยู่ ให้สร้างใหม่
await this.notificationQueue.add(
'process-digest',
{ userId: data.userId, type: data.type },
{
delay: this.DIGEST_DELAY,
jobId: `digest-${data.type}-${data.userId}-${Date.now()}`,
},
);
// Set Lock ไว้ตามเวลา Delay เพื่อไม่ให้สร้าง Job ซ้ำ
await this.redis.set(lockKey, '1', 'PX', this.DIGEST_DELAY);
this.logger.log(
`Scheduled digest for User ${data.userId} (${data.type}) in ${this.DIGEST_DELAY}ms`,
);
}
}
/**
* ประมวลผล Digest (ส่งแบบรวม)
*/
private async handleProcessDigest(userId: number, type: 'EMAIL' | 'LINE') {
const key = `digest:${type}:${userId}`;
const lockKey = `digest:lock:${type}:${userId}`;
// 1. ดึงข้อความทั้งหมดจาก Redis และลบออกทันที
const messagesRaw = await this.redis.lrange(key, 0, -1);
await this.redis.del(key);
await this.redis.del(lockKey); // Clear lock
if (!messagesRaw || messagesRaw.length === 0) return;
const messages: NotificationPayload[] = messagesRaw.map((m) =>
JSON.parse(m),
);
const user = await this.userService.findOne(userId);
if (type === 'EMAIL') {
await this.sendEmailDigest(user, messages);
} else if (type === 'LINE') {
await this.sendLineDigest(user, messages);
}
}
// =====================================================
// SENDERS (Immediate & Digest)
// =====================================================
private async sendEmailImmediate(user: any, data: NotificationPayload) {
if (!user.email) return;
await this.mailerTransport.sendMail({
from: '"LCBP3 DMS" <no-reply@np-dms.work>',
to: user.email,
subject: `[DMS] ${data.title}`,
html: `
<h3>${data.title}</h3>
<p>${data.message}</p>
<br/>
<a href="${data.link}">คลิกเพื่อดูรายละเอียด</a>
`,
html: `<h3>${data.title}</h3><p>${data.message}</p><br/><a href="${data.link}">คลิกเพื่อดูรายละเอียด</a>`,
});
this.logger.log(`Email sent to ${user.email}`);
}
private async handleSendLine(data: any) {
const user = await this.userService.findOne(data.userId);
// ตรวจสอบว่า User มี Line ID หรือไม่ (หรือใช้ Group Token ถ้าเป็นระบบรวม)
// ในที่นี้สมมติว่าเรายิงเข้า n8n webhook เพื่อจัดการต่อ
const n8nWebhookUrl = this.configService.get('N8N_LINE_WEBHOOK_URL');
private async sendEmailDigest(user: any, messages: NotificationPayload[]) {
if (!user.email) return;
if (!n8nWebhookUrl) {
this.logger.warn('N8N_LINE_WEBHOOK_URL not configured');
return;
}
// สร้าง HTML List
const listItems = messages
.map(
(msg) =>
`<li><strong>${msg.title}</strong>: ${msg.message} <a href="${msg.link}">[View]</a></li>`,
)
.join('');
await this.mailerTransport.sendMail({
from: '"LCBP3 DMS" <no-reply@np-dms.work>',
to: user.email,
subject: `[DMS Summary] คุณมีการแจ้งเตือนใหม่ ${messages.length} รายการ`,
html: `
<h3>สรุปรายการแจ้งเตือน (Digest)</h3>
<ul>${listItems}</ul>
<p>คุณได้รับอีเมลนี้เพราะเปิดใช้งานโหมดสรุปรายการ</p>
`,
});
this.logger.log(
`Digest Email sent to ${user.email} (${messages.length} items)`,
);
}
private async sendLineImmediate(user: any, data: NotificationPayload) {
const n8nWebhookUrl = this.configService.get('N8N_LINE_WEBHOOK_URL');
if (!n8nWebhookUrl) return;
try {
await axios.post(n8nWebhookUrl, {
userId: user.user_id, // หรือ user.lineId ถ้ามี
userId: user.user_id,
message: `${data.title}\n${data.message}`,
link: data.link,
isDigest: false,
});
this.logger.log(`Line notification sent via n8n for user ${data.userId}`);
} catch (error: any) {
throw new Error(`Failed to send Line notification: ${error.message}`);
} catch (error) {
// ✅ แก้ไขตรงนี้ด้วย: Type Casting (error as Error)
this.logger.error(`Line Error: ${(error as Error).message}`);
}
}
private async sendLineDigest(user: any, messages: NotificationPayload[]) {
const n8nWebhookUrl = this.configService.get('N8N_LINE_WEBHOOK_URL');
if (!n8nWebhookUrl) return;
const summary = messages.map((m, i) => `${i + 1}. ${m.title}`).join('\n');
try {
await axios.post(n8nWebhookUrl, {
userId: user.user_id,
message: `สรุป ${messages.length} รายการใหม่:\n${summary}`,
link: 'https://lcbp3.np-dms.work/notifications',
isDigest: true,
});
} catch (error) {
// ✅ แก้ไขตรงนี้ด้วย: Type Casting (error as Error)
this.logger.error(`Line Digest Error: ${(error as Error).message}`);
}
}
}

View File

@@ -1,4 +1,5 @@
// File: src/modules/notification/notification.service.ts
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bullmq';
@@ -22,9 +23,9 @@ export interface NotificationJobData {
title: string;
message: string;
type: 'EMAIL' | 'LINE' | 'SYSTEM'; // ช่องทางหลักที่ต้องการส่ง (Trigger Type)
entityType?: string; // e.g., 'rfa', 'correspondence'
entityId?: number; // e.g., rfa_id
link?: string; // Deep link to frontend page
entityType?: string;
entityId?: number;
link?: string;
}
@Injectable()
@@ -37,109 +38,57 @@ export class NotificationService {
private notificationRepo: Repository<Notification>,
@InjectRepository(User)
private userRepo: Repository<User>,
@InjectRepository(UserPreference)
private userPrefRepo: Repository<UserPreference>,
// ไม่ต้อง Inject UserPrefRepo แล้ว เพราะ Processor จะจัดการเอง
private notificationGateway: NotificationGateway,
) {}
/**
* ส่งการแจ้งเตือน (Centralized Notification Sender)
* 1. บันทึก DB (System Log)
* 2. ส่ง Real-time (WebSocket)
* 3. ส่ง External (Email/Line) ผ่าน Queue ตาม User Preference
*/
async send(data: NotificationJobData): Promise<void> {
try {
// ---------------------------------------------------------
// 1. สร้าง Entity และบันทึกลง DB (เพื่อให้มี History ในระบบ)
// 1. สร้าง Entity และบันทึกลง DB (System Log)
// ---------------------------------------------------------
const notification = this.notificationRepo.create({
userId: data.userId,
title: data.title,
message: data.message,
notificationType: NotificationType.SYSTEM, // ใน DB เก็บเป็น SYSTEM เสมอเพื่อแสดงใน App
notificationType: NotificationType.SYSTEM,
entityType: data.entityType,
entityId: data.entityId,
isRead: false,
// link: data.link // ถ้า Entity มี field link ให้ใส่ด้วย
});
const savedNotification = await this.notificationRepo.save(notification);
// ---------------------------------------------------------
// 2. Real-time Push (WebSocket) -> ส่งให้ User ทันทีถ้า Online
// 2. Real-time Push (WebSocket)
// ---------------------------------------------------------
this.notificationGateway.sendToUser(data.userId, savedNotification);
// ---------------------------------------------------------
// 3. ตรวจสอบ User Preferences เพื่อส่งช่องทางอื่น (Email/Line)
// 3. Push Job ลง Redis BullMQ (Dispatch Logic)
// เปลี่ยนชื่อ Job เป็น 'dispatch-notification' ตาม Processor
// ---------------------------------------------------------
const userPref = await this.userPrefRepo.findOne({
where: { userId: data.userId },
});
// ใช้ Nullish Coalescing Operator (??)
// ถ้าไม่มีค่า (undefined/null) ให้ Default เป็น true
const shouldSendEmail = userPref?.notifyEmail ?? true;
const shouldSendLine = userPref?.notifyLine ?? true;
const jobs = [];
// ---------------------------------------------------------
// 4. เตรียม Job สำหรับ Email Queue
// เงื่อนไข: User เปิดรับ Email และ Noti นี้ไม่ได้บังคับส่งแค่ LINE
// ---------------------------------------------------------
if (shouldSendEmail && data.type !== 'LINE') {
jobs.push({
name: 'send-email',
data: {
...data,
notificationId: savedNotification.id,
target: 'EMAIL',
await this.notificationQueue.add(
'dispatch-notification',
{
...data,
notificationId: savedNotification.id, // ส่ง ID ไปด้วยเผื่อใช้ Tracking
},
{
attempts: 3,
backoff: {
type: 'exponential',
delay: 5000,
},
opts: {
attempts: 3, // ลองใหม่ 3 ครั้งถ้าล่ม (Resilience)
backoff: {
type: 'exponential',
delay: 5000, // รอ 5s, 10s, 20s...
},
removeOnComplete: true, // ลบ Job เมื่อเสร็จ (ประหยัด Redis Memory)
},
});
}
removeOnComplete: true,
},
);
// ---------------------------------------------------------
// 5. เตรียม Job สำหรับ Line Queue
// เงื่อนไข: User เปิดรับ Line และ Noti นี้ไม่ได้บังคับส่งแค่ EMAIL
// ---------------------------------------------------------
if (shouldSendLine && data.type !== 'EMAIL') {
jobs.push({
name: 'send-line',
data: {
...data,
notificationId: savedNotification.id,
target: 'LINE',
},
opts: {
attempts: 3,
backoff: { type: 'fixed', delay: 3000 },
removeOnComplete: true,
},
});
}
// ---------------------------------------------------------
// 6. Push Jobs ลง Redis BullMQ
// ---------------------------------------------------------
if (jobs.length > 0) {
await this.notificationQueue.addBulk(jobs);
this.logger.debug(
`Queued ${jobs.length} external notifications for user ${data.userId}`,
);
}
this.logger.debug(`Dispatched notification job for user ${data.userId}`);
} catch (error) {
// Error Handling: ไม่ Throw เพื่อไม่ให้ Flow หลัก (เช่น การสร้างเอกสาร) พัง
// แต่บันทึก Error ไว้ตรวจสอบ
this.logger.error(
`Failed to process notification for user ${data.userId}`,
(error as Error).stack,
@@ -147,9 +96,8 @@ export class NotificationService {
}
}
/**
* ดึงรายการแจ้งเตือนของ User (สำหรับ Controller)
*/
// ... (ส่วน findAll, markAsRead, cleanupOldNotifications เหมือนเดิม ไม่ต้องแก้) ...
async findAll(userId: number, searchDto: SearchNotificationDto) {
const { page = 1, limit = 20, isRead } = searchDto;
const skip = (page - 1) * limit;
@@ -161,14 +109,11 @@ export class NotificationService {
.take(limit)
.skip(skip);
// Filter by Read Status (ถ้ามีการส่งมา)
if (isRead !== undefined) {
queryBuilder.andWhere('notification.isRead = :isRead', { isRead });
}
const [items, total] = await queryBuilder.getManyAndCount();
// นับจำนวนที่ยังไม่ได้อ่านทั้งหมด (เพื่อแสดง Badge ที่กระดิ่ง)
const unreadCount = await this.notificationRepo.count({
where: { userId, isRead: false },
});
@@ -185,9 +130,6 @@ export class NotificationService {
};
}
/**
* อ่านแจ้งเตือน (Mark as Read)
*/
async markAsRead(id: number, userId: number): Promise<void> {
const notification = await this.notificationRepo.findOne({
where: { id, userId },
@@ -200,15 +142,9 @@ export class NotificationService {
if (!notification.isRead) {
notification.isRead = true;
await this.notificationRepo.save(notification);
// Update Unread Count via WebSocket (Optional)
// this.notificationGateway.sendUnreadCount(userId, ...);
}
}
/**
* อ่านทั้งหมด (Mark All as Read)
*/
async markAllAsRead(userId: number): Promise<void> {
await this.notificationRepo.update(
{ userId, isRead: false },
@@ -216,10 +152,6 @@ export class NotificationService {
);
}
/**
* ลบการแจ้งเตือนที่เก่าเกินกำหนด (ใช้กับ Cron Job Cleanup)
* เก็บไว้ 90 วัน
*/
async cleanupOldNotifications(days: number = 90): Promise<number> {
const dateLimit = new Date();
dateLimit.setDate(dateLimit.getDate() - days);

View File

@@ -64,6 +64,7 @@ export class UserService {
async findOne(id: number): Promise<User> {
const user = await this.usersRepository.findOne({
where: { user_id: id },
relations: ['preferences', 'roles'], // [IMPORTANT] ต้องโหลด preferences มาด้วย
});
if (!user) {