Compare commits

..

11 Commits

Author SHA1 Message Date
266b7b33de Refactor config management to use Drizzle ORM
- Replaced Sequelize with Drizzle ORM in config-related routes and models.
- Updated database queries to use Drizzle's syntax for selecting, inserting, updating, and deleting configurations.
- Removed the ConfigModel class and replaced it with direct database interactions.
- Introduced nanoid for generating unique IDs for new configurations.
- Added new routes for managing marks, including CRUD operations and versioning.
- Implemented transaction handling for critical operations in the MarkModel.
- Enhanced error handling and validation in routes.
2026-02-05 16:31:11 +08:00
5200cf4c38 修复用户应用键的分隔符,从 '-' 更改为 '--',以保持一致性并优化 WebSocket 连接管理 2026-02-05 14:08:45 +08:00
bf436f05e3 优化 WebSocket 连接管理,确保在注册新连接时关闭旧连接 2026-02-05 13:38:55 +08:00
bd7525efb0 添加心跳机制以保持 WebSocket 连接,优化连接关闭时的资源清理 2026-02-05 04:58:28 +08:00
f616045625 优化用户代理逻辑,移除非管理员用户的敏感数据 2026-02-05 04:50:54 +08:00
a51d04341e 添加 @kevisual/api 依赖,更新 WebSocket 消息发送逻辑,支持上下文参数 2026-02-05 04:06:34 +08:00
7bbefd8a4a 添加自动检测最新版本功能,更新应用信息时支持检测参数 2026-02-05 01:07:44 +08:00
db5c5a89b3 clear 2026-02-04 19:48:02 +08:00
86d4c7f75b 移除不再支持的文件扩展名 '.mjs' 从文本内容类型列表中 2026-02-04 19:45:01 +08:00
cbc9b54284 update 2026-02-04 03:08:53 +08:00
b1d3ca241c 优化 token 处理逻辑,统一过期时间字段命名 2026-02-03 17:09:09 +08:00
23 changed files with 1130 additions and 714 deletions

View File

@@ -49,6 +49,7 @@
"dependencies": {
"@kevisual/ai": "^0.0.24",
"@kevisual/auth": "^2.0.3",
"@kevisual/js-filter": "^0.0.5",
"@kevisual/query": "^0.0.39",
"@types/busboy": "^1.5.4",
"@types/send": "^1.2.1",
@@ -71,7 +72,8 @@
"zod-to-json-schema": "^3.25.1"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.980.0",
"@aws-sdk/client-s3": "^3.981.0",
"@kevisual/api": "^0.0.44",
"@kevisual/code-center-module": "0.0.24",
"@kevisual/context": "^0.0.4",
"@kevisual/file-listener": "^0.0.2",
@@ -79,7 +81,7 @@
"@kevisual/logger": "^0.0.4",
"@kevisual/oss": "0.0.19",
"@kevisual/permission": "^0.0.4",
"@kevisual/router": "0.0.66",
"@kevisual/router": "0.0.70",
"@kevisual/types": "^0.0.12",
"@kevisual/use-config": "^1.0.30",
"@types/archiver": "^7.0.0",

552
pnpm-lock.yaml generated
View File

@@ -18,6 +18,9 @@ importers:
'@kevisual/auth':
specifier: ^2.0.3
version: 2.0.3
'@kevisual/js-filter':
specifier: ^0.0.5
version: 0.0.5
'@kevisual/query':
specifier: ^0.0.39
version: 0.0.39
@@ -80,8 +83,11 @@ importers:
version: 3.25.1(zod@4.3.6)
devDependencies:
'@aws-sdk/client-s3':
specifier: ^3.980.0
version: 3.980.0
specifier: ^3.981.0
version: 3.981.0
'@kevisual/api':
specifier: ^0.0.44
version: 0.0.44
'@kevisual/code-center-module':
specifier: 0.0.24
version: 0.0.24(dotenv@17.2.3)
@@ -104,8 +110,8 @@ importers:
specifier: ^0.0.4
version: 0.0.4
'@kevisual/router':
specifier: 0.0.66
version: 0.0.66(typescript@5.9.3)
specifier: 0.0.70
version: 0.0.70
'@kevisual/types':
specifier: ^0.0.12
version: 0.0.12
@@ -229,8 +235,8 @@ packages:
'@aws-crypto/util@5.2.0':
resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
'@aws-sdk/client-s3@3.980.0':
resolution: {integrity: sha512-ch8QqKehyn1WOYbd8LyDbWjv84Z9OEj9qUxz8q3IOCU3ftAVkVR0wAuN96a1xCHnpOJcQZo3rOB08RlyKdkGxQ==}
'@aws-sdk/client-s3@3.981.0':
resolution: {integrity: sha512-zX3Xqm7V30J1D2II7WBL23SyqIIMD0wMzpiE+VosBxH6fAeXgrjIwSudCypNgnE1EK9OZoZMT3mJtkbUqUDdaA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/client-sso@3.980.0':
@@ -325,8 +331,8 @@ packages:
resolution: {integrity: sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==}
engines: {node: '>=20.0.0'}
'@aws-sdk/signature-v4-multi-region@3.980.0':
resolution: {integrity: sha512-tO2jBj+ZIVM0nEgi1SyxWtaYGpuAJdsrugmWcI3/U2MPWCYsrvKasUo0026NvJJao38wyUq9B8XTG8Xu53j/VA==}
'@aws-sdk/signature-v4-multi-region@3.981.0':
resolution: {integrity: sha512-T/+h9df0DALAXXP+YfZ8bgmH6cEN7HAg6BqHe3t38GhHgQ1HULXwK5XMhiLWiHpytDdhLqiVH41SRgW8ynBl6Q==}
engines: {node: '>=20.0.0'}
'@aws-sdk/token-providers@3.980.0':
@@ -345,6 +351,10 @@ packages:
resolution: {integrity: sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/util-endpoints@3.981.0':
resolution: {integrity: sha512-a8nXh/H3/4j+sxhZk+N3acSDlgwTVSZbX9i55dx41gI1H+geuonuRG+Shv3GZsCb46vzc08RK2qC78ypO8uRlg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/util-locate-window@3.965.4':
resolution: {integrity: sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog==}
engines: {node: '>=20.0.0'}
@@ -369,14 +379,6 @@ packages:
resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==}
engines: {node: '>=18.0.0'}
'@babel/code-frame@7.28.6':
resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
'@drizzle-team/brocli@0.10.2':
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
@@ -839,12 +841,12 @@ packages:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@kevisual/ai@0.0.24':
resolution: {integrity: sha512-7jvZk1/L//VIClK7usuNgN4ZA9Etgbooka1Sj5quE/0UywR+NNnwqXVZ89Y1fBhI1TkhauDsdJBAtcQ7r/vbVw==}
'@kevisual/api@0.0.44':
resolution: {integrity: sha512-KA2b17pxW1pTPWa4zsTSRTiGTmwdkIesV1ig51MyISUllita5VPqZ6UYYDJQTHuPzYcIkuodQ9iWTEZNM9AkFw==}
'@kevisual/auth@1.0.5':
resolution: {integrity: sha512-GwsLj7unKXi7lmMiIIgdig4LwwLiDJnOy15HHZR5gMbyK6s5/uJiMY5RXPB2+onGzTNDqFo/hXjsD2wkerHPVg==}
@@ -860,13 +862,12 @@ packages:
'@kevisual/context@0.0.4':
resolution: {integrity: sha512-HJeLeZQLU+7tCluSfOyvkgKLs0HjCZrdJlZgEgKRSa8XTwZfMAUt6J7qZTbrZAHBlPtX68EPu/PI8JMCeu3WAQ==}
'@kevisual/dts@0.0.3':
resolution: {integrity: sha512-4T/m2LqhtwWEW+lWmg7jLxKFW7VtIAftsWFDDZvh10bZunqFf8iXxChHcVSQWikghJb4cq1IkWzPkvc2l+Asdw==}
hasBin: true
'@kevisual/file-listener@0.0.2':
resolution: {integrity: sha512-1XVoXBtNi813x6JXVT0xJeAzIjVJtputCyArgczhzH6KYX4P3W60QIYh45riuMFBynO21ULWnmJJqZmPmeKsNQ==}
'@kevisual/js-filter@0.0.5':
resolution: {integrity: sha512-+S+Sf3K/aP6XtZI2s7TgKOr35UuvUvtpJ9YDW30a+mY0/N8gRuzyKhieBzQN7Ykayzz70uoMavBXut2rUlLgzw==}
'@kevisual/load@0.0.6':
resolution: {integrity: sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA==}
@@ -903,8 +904,8 @@ packages:
'@kevisual/router@0.0.60':
resolution: {integrity: sha512-2v/ZzUstsaq+Uqo+tZX9ys5E+/2erPggCtljv9jTb3NA88ZdHsYUAsd5wUFvLtf9QucpJCzyWEt+InDV/98FKw==}
'@kevisual/router@0.0.66':
resolution: {integrity: sha512-yoiCfKJ8yxrXToh8ud1+/JFqlRexrZmJ0PhofQX3jyfmmyEBQQJFL+2UYewm4FxbG3l7ndBC/NIhu1v5CdwxiQ==}
'@kevisual/router@0.0.70':
resolution: {integrity: sha512-vXlIj9jRufhcIfeuPWemjSI+dxdzSmIBq5eRxQzqEfAJ7k+mBPhoI4KxH8vHnwyL30bqm8EdODL/p6Wg8uBw3g==}
'@kevisual/types@0.0.12':
resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==}
@@ -978,184 +979,6 @@ packages:
'@pm2/pm2-version-check@1.0.4':
resolution: {integrity: sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==}
'@rollup/plugin-commonjs@28.0.9':
resolution: {integrity: sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==}
engines: {node: '>=16.0.0 || 14 >= 14.17'}
peerDependencies:
rollup: ^2.68.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/plugin-node-resolve@16.0.3':
resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^2.78.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/plugin-typescript@12.3.0':
resolution: {integrity: sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^2.14.0||^3.0.0||^4.0.0
tslib: '*'
typescript: '>=3.7.0'
peerDependenciesMeta:
rollup:
optional: true
tslib:
optional: true
'@rollup/pluginutils@5.3.0':
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/rollup-android-arm-eabi@4.57.1':
resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.57.1':
resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.57.1':
resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.57.1':
resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.57.1':
resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.57.1':
resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.57.1':
resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.57.1':
resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.57.1':
resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.57.1':
resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.57.1':
resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.57.1':
resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.57.1':
resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.57.1':
resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.57.1':
resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.57.1':
resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.57.1':
resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.57.1':
resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.57.1':
resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.57.1':
resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==}
cpu: [x64]
os: [openbsd]
'@rollup/rollup-openharmony-arm64@4.57.1':
resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.57.1':
resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.57.1':
resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.57.1':
resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.57.1':
resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==}
cpu: [x64]
os: [win32]
'@smithy/abort-controller@4.2.8':
resolution: {integrity: sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==}
engines: {node: '>=18.0.0'}
@@ -1399,9 +1222,6 @@ packages:
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/jsonwebtoken@9.0.10':
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
@@ -1423,9 +1243,6 @@ packages:
'@types/readdir-glob@1.1.5':
resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==}
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
'@types/semver@7.7.1':
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
@@ -1640,9 +1457,6 @@ packages:
commander@2.15.1:
resolution: {integrity: sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==}
commondir@1.0.1:
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
compress-commons@6.0.2:
resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==}
engines: {node: '>= 14'}
@@ -1767,10 +1581,6 @@ packages:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
@@ -1989,9 +1799,6 @@ packages:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
@@ -2041,15 +1848,6 @@ packages:
fclone@1.0.11:
resolution: {integrity: sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^4.0.2
peerDependenciesMeta:
picomatch:
optional: true
file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
@@ -2100,6 +1898,10 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
fuse.js@7.1.0:
resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==}
engines: {node: '>=10'}
get-intrinsic@1.2.4:
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
engines: {node: '>= 0.4'}
@@ -2170,10 +1972,6 @@ packages:
resolution: {integrity: sha512-WemPi9/WfyMwZs+ZUXdiwcCh9Y+m7L+8vki9MzDw3jJ+W9Lc+12HGsd368Qc1vZi1xwW8BWMMsnK5efYKPdt4g==}
engines: {node: '>=16.9.0'}
hono@4.11.7:
resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==}
engines: {node: '>=16.9.0'}
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
@@ -2250,16 +2048,10 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-module@1.0.0:
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-reference@1.2.1:
resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
@@ -2280,9 +2072,6 @@ packages:
js-git@0.7.8:
resolution: {integrity: sha512-+E5ZH/HeRnoc/LW0AmAyhU+mNcWBzAKE+30+IDMLSLbbK+Tdt02AdkOKq9u15rlJsDEGFqtgckc8ZM59LhhiUA==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
@@ -2358,9 +2147,6 @@ packages:
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
engines: {node: '>=12'}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
@@ -2511,6 +2297,9 @@ packages:
pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
path-browserify-esm@1.0.6:
resolution: {integrity: sha512-9nUwYvvu/yq1PYrUyYCihNWmpzacaRYF6gGbjLWErrZ4MRDWyfPN7RpE8E7tsw8eqBU/rr7mcoTXbS+Vih8uUA==}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
@@ -2562,9 +2351,6 @@ packages:
pgpass@1.0.5:
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.2:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
@@ -2712,18 +2498,6 @@ packages:
retry-as-promised@7.0.4:
resolution: {integrity: sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==}
rollup-plugin-dts@6.3.0:
resolution: {integrity: sha512-d0UrqxYd8KyZ6i3M2Nx7WOMy708qsV/7fTHMHxCMCBOAe3V/U7OMPu5GkX8hC+cmkHhzGnfeYongl1IgiooddA==}
engines: {node: '>=16'}
peerDependencies:
rollup: ^3.29.4 || ^4
typescript: ^4.5 || ^5.0
rollup@4.57.1:
resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
run-series@1.1.9:
resolution: {integrity: sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==}
@@ -2864,6 +2638,9 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
spark-md5@3.0.2:
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
split-on-first@1.1.0:
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
engines: {node: '>=6'}
@@ -2999,11 +2776,6 @@ packages:
tx2@1.0.5:
resolution: {integrity: sha512-sJ24w0y03Md/bxzK4FU8J8JveYYUbSs2FViLJ2D/8bytSiyPRbuE3DyL/9UKYXTZlV3yXq0L8GLlhobTnekCVg==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undefsafe@2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
@@ -3169,7 +2941,7 @@ snapshots:
'@smithy/util-utf8': 2.3.0
tslib: 2.8.1
'@aws-sdk/client-s3@3.980.0':
'@aws-sdk/client-s3@3.981.0':
dependencies:
'@aws-crypto/sha1-browser': 5.2.0
'@aws-crypto/sha256-browser': 5.2.0
@@ -3187,9 +2959,9 @@ snapshots:
'@aws-sdk/middleware-ssec': 3.972.3
'@aws-sdk/middleware-user-agent': 3.972.5
'@aws-sdk/region-config-resolver': 3.972.3
'@aws-sdk/signature-v4-multi-region': 3.980.0
'@aws-sdk/signature-v4-multi-region': 3.981.0
'@aws-sdk/types': 3.973.1
'@aws-sdk/util-endpoints': 3.980.0
'@aws-sdk/util-endpoints': 3.981.0
'@aws-sdk/util-user-agent-browser': 3.972.3
'@aws-sdk/util-user-agent-node': 3.972.3
'@smithy/config-resolver': 4.4.6
@@ -3542,7 +3314,7 @@ snapshots:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@aws-sdk/signature-v4-multi-region@3.980.0':
'@aws-sdk/signature-v4-multi-region@3.981.0':
dependencies:
'@aws-sdk/middleware-sdk-s3': 3.972.5
'@aws-sdk/types': 3.973.1
@@ -3580,6 +3352,14 @@ snapshots:
'@smithy/util-endpoints': 3.2.8
tslib: 2.8.1
'@aws-sdk/util-endpoints@3.981.0':
dependencies:
'@aws-sdk/types': 3.973.1
'@smithy/types': 4.12.0
'@smithy/url-parser': 4.2.8
'@smithy/util-endpoints': 3.2.8
tslib: 2.8.1
'@aws-sdk/util-locate-window@3.965.4':
dependencies:
tslib: 2.8.1
@@ -3607,16 +3387,6 @@ snapshots:
'@aws/lambda-invoke-store@0.2.3': {}
'@babel/code-frame@7.28.6':
dependencies:
'@babel/helper-validator-identifier': 7.28.5
js-tokens: 4.0.0
picocolors: 1.1.1
optional: true
'@babel/helper-validator-identifier@7.28.5':
optional: true
'@drizzle-team/brocli@0.10.2': {}
'@esbuild-kit/core-utils@3.3.2':
@@ -3862,14 +3632,23 @@ snapshots:
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@jridgewell/sourcemap-codec@1.5.5': {}
'@kevisual/ai@0.0.24':
dependencies:
'@kevisual/logger': 0.0.4
'@kevisual/permission': 0.0.3
'@kevisual/query': 0.0.38
'@kevisual/api@0.0.44':
dependencies:
'@kevisual/js-filter': 0.0.5
'@kevisual/load': 0.0.6
es-toolkit: 1.44.0
eventemitter3: 5.0.4
fuse.js: 7.1.0
nanoid: 5.1.6
path-browserify-esm: 1.0.6
spark-md5: 3.0.2
'@kevisual/auth@1.0.5': {}
'@kevisual/auth@2.0.3': {}
@@ -3928,17 +3707,6 @@ snapshots:
'@kevisual/context@0.0.4': {}
'@kevisual/dts@0.0.3(typescript@5.9.3)':
dependencies:
'@rollup/plugin-commonjs': 28.0.9(rollup@4.57.1)
'@rollup/plugin-node-resolve': 16.0.3(rollup@4.57.1)
'@rollup/plugin-typescript': 12.3.0(rollup@4.57.1)(tslib@2.8.1)(typescript@5.9.3)
rollup: 4.57.1
rollup-plugin-dts: 6.3.0(rollup@4.57.1)(typescript@5.9.3)
tslib: 2.8.1
transitivePeerDependencies:
- typescript
'@kevisual/file-listener@0.0.2(dotenv@17.2.3)':
dependencies:
'@kevisual/code-center-module': 0.0.20(dotenv@17.2.3)
@@ -3963,6 +3731,8 @@ snapshots:
- tedious
- utf-8-validate
'@kevisual/js-filter@0.0.5': {}
'@kevisual/load@0.0.6':
dependencies:
eventemitter3: 5.0.4
@@ -4013,12 +3783,9 @@ snapshots:
dependencies:
hono: 4.11.5
'@kevisual/router@0.0.66(typescript@5.9.3)':
'@kevisual/router@0.0.70':
dependencies:
'@kevisual/dts': 0.0.3(typescript@5.9.3)
hono: 4.11.7
transitivePeerDependencies:
- typescript
es-toolkit: 1.44.0
'@kevisual/types@0.0.12': {}
@@ -4108,120 +3875,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@rollup/plugin-commonjs@28.0.9(rollup@4.57.1)':
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.57.1)
commondir: 1.0.1
estree-walker: 2.0.2
fdir: 6.5.0(picomatch@4.0.2)
is-reference: 1.2.1
magic-string: 0.30.21
picomatch: 4.0.2
optionalDependencies:
rollup: 4.57.1
'@rollup/plugin-node-resolve@16.0.3(rollup@4.57.1)':
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.57.1)
'@types/resolve': 1.20.2
deepmerge: 4.3.1
is-module: 1.0.0
resolve: 1.22.8
optionalDependencies:
rollup: 4.57.1
'@rollup/plugin-typescript@12.3.0(rollup@4.57.1)(tslib@2.8.1)(typescript@5.9.3)':
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.57.1)
resolve: 1.22.8
typescript: 5.9.3
optionalDependencies:
rollup: 4.57.1
tslib: 2.8.1
'@rollup/pluginutils@5.3.0(rollup@4.57.1)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.2
optionalDependencies:
rollup: 4.57.1
'@rollup/rollup-android-arm-eabi@4.57.1':
optional: true
'@rollup/rollup-android-arm64@4.57.1':
optional: true
'@rollup/rollup-darwin-arm64@4.57.1':
optional: true
'@rollup/rollup-darwin-x64@4.57.1':
optional: true
'@rollup/rollup-freebsd-arm64@4.57.1':
optional: true
'@rollup/rollup-freebsd-x64@4.57.1':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.57.1':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.57.1':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-arm64-musl@4.57.1':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-loong64-musl@4.57.1':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-ppc64-musl@4.57.1':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.57.1':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-x64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-x64-musl@4.57.1':
optional: true
'@rollup/rollup-openbsd-x64@4.57.1':
optional: true
'@rollup/rollup-openharmony-arm64@4.57.1':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.57.1':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.57.1':
optional: true
'@rollup/rollup-win32-x64-gnu@4.57.1':
optional: true
'@rollup/rollup-win32-x64-msvc@4.57.1':
optional: true
'@smithy/abort-controller@4.2.8':
dependencies:
'@smithy/types': 4.12.0
@@ -4588,8 +4241,6 @@ snapshots:
dependencies:
'@types/ms': 0.7.34
'@types/estree@1.0.8': {}
'@types/jsonwebtoken@9.0.10':
dependencies:
'@types/ms': 0.7.34
@@ -4619,8 +4270,6 @@ snapshots:
dependencies:
'@types/node': 25.2.0
'@types/resolve@1.20.2': {}
'@types/semver@7.7.1': {}
'@types/send@1.2.1':
@@ -4852,8 +4501,6 @@ snapshots:
commander@2.15.1: {}
commondir@1.0.1: {}
compress-commons@6.0.2:
dependencies:
crc-32: 1.2.2
@@ -4937,8 +4584,6 @@ snapshots:
deep-extend@0.6.0: {}
deepmerge@4.3.1: {}
define-data-property@1.1.4:
dependencies:
es-define-property: 1.0.0
@@ -5142,8 +4787,6 @@ snapshots:
estraverse@5.3.0: {}
estree-walker@2.0.2: {}
esutils@2.0.3: {}
etag@1.8.1: {}
@@ -5180,10 +4823,6 @@ snapshots:
fclone@1.0.11: {}
fdir@6.5.0(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
file-uri-to-path@1.0.0: {}
fill-range@7.1.1:
@@ -5226,6 +4865,8 @@ snapshots:
function-bind@1.1.2: {}
fuse.js@7.1.0: {}
get-intrinsic@1.2.4:
dependencies:
es-errors: 1.3.0
@@ -5296,8 +4937,6 @@ snapshots:
hono@4.11.5: {}
hono@4.11.7: {}
http-errors@2.0.1:
dependencies:
depd: 2.0.0
@@ -5382,14 +5021,8 @@ snapshots:
dependencies:
is-extglob: 2.1.1
is-module@1.0.0: {}
is-number@7.0.0: {}
is-reference@1.2.1:
dependencies:
'@types/estree': 1.0.8
is-stream@2.0.1: {}
is-typed-array@1.1.13:
@@ -5413,9 +5046,6 @@ snapshots:
git-sha1: 0.1.2
pako: 0.2.9
js-tokens@4.0.0:
optional: true
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
@@ -5491,10 +5121,6 @@ snapshots:
luxon@3.7.2: {}
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
mime-db@1.52.0: {}
mime-db@1.54.0: {}
@@ -5657,6 +5283,8 @@ snapshots:
pako@0.2.9: {}
path-browserify-esm@1.0.6: {}
path-key@3.1.1: {}
path-parse@1.0.7: {}
@@ -5705,9 +5333,6 @@ snapshots:
dependencies:
split2: 4.2.0
picocolors@1.1.1:
optional: true
picomatch@4.0.2: {}
pidusage@2.0.21:
@@ -5929,45 +5554,6 @@ snapshots:
retry-as-promised@7.0.4: {}
rollup-plugin-dts@6.3.0(rollup@4.57.1)(typescript@5.9.3):
dependencies:
magic-string: 0.30.21
rollup: 4.57.1
typescript: 5.9.3
optionalDependencies:
'@babel/code-frame': 7.28.6
rollup@4.57.1:
dependencies:
'@types/estree': 1.0.8
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.57.1
'@rollup/rollup-android-arm64': 4.57.1
'@rollup/rollup-darwin-arm64': 4.57.1
'@rollup/rollup-darwin-x64': 4.57.1
'@rollup/rollup-freebsd-arm64': 4.57.1
'@rollup/rollup-freebsd-x64': 4.57.1
'@rollup/rollup-linux-arm-gnueabihf': 4.57.1
'@rollup/rollup-linux-arm-musleabihf': 4.57.1
'@rollup/rollup-linux-arm64-gnu': 4.57.1
'@rollup/rollup-linux-arm64-musl': 4.57.1
'@rollup/rollup-linux-loong64-gnu': 4.57.1
'@rollup/rollup-linux-loong64-musl': 4.57.1
'@rollup/rollup-linux-ppc64-gnu': 4.57.1
'@rollup/rollup-linux-ppc64-musl': 4.57.1
'@rollup/rollup-linux-riscv64-gnu': 4.57.1
'@rollup/rollup-linux-riscv64-musl': 4.57.1
'@rollup/rollup-linux-s390x-gnu': 4.57.1
'@rollup/rollup-linux-x64-gnu': 4.57.1
'@rollup/rollup-linux-x64-musl': 4.57.1
'@rollup/rollup-openbsd-x64': 4.57.1
'@rollup/rollup-openharmony-arm64': 4.57.1
'@rollup/rollup-win32-arm64-msvc': 4.57.1
'@rollup/rollup-win32-ia32-msvc': 4.57.1
'@rollup/rollup-win32-x64-gnu': 4.57.1
'@rollup/rollup-win32-x64-msvc': 4.57.1
fsevents: 2.3.3
run-series@1.1.9: {}
safe-buffer@5.1.2: {}
@@ -6119,6 +5705,8 @@ snapshots:
source-map@0.6.1: {}
spark-md5@3.0.2: {}
split-on-first@1.1.0: {}
split2@4.2.0: {}
@@ -6250,8 +5838,6 @@ snapshots:
json-stringify-safe: 5.0.1
optional: true
typescript@5.9.3: {}
undefsafe@2.0.5: {}
undici-types@7.16.0: {}

View File

@@ -66,8 +66,8 @@ export class User extends Model {
accessToken: token.accessToken,
refreshToken: token.refreshToken,
token: token.accessToken,
accessTokenExpiresAt: token.accessTokenExpiresAt,
refreshTokenExpiresAt: token.refreshTokenExpiresAt,
refreshTokenExpiresIn: token.refreshTokenExpiresIn,
accessTokenExpiresIn: token.accessTokenExpiresIn,
};
}
/**

View File

@@ -76,9 +76,9 @@ interface Store<T> {
type TokenData = {
accessToken: string;
accessTokenExpiresAt: number;
accessTokenExpiresIn?: number;
refreshToken?: string;
refreshTokenExpiresAt?: number;
refreshTokenExpiresIn?: number;
}
export class RedisTokenStore implements Store<OauthUser> {
redis: Redis;
@@ -170,20 +170,20 @@ export class RedisTokenStore implements Store<OauthUser> {
await this.set(accessToken, JSON.stringify(value), expire);
await this.set(userPrefix + ':token:' + accessToken, accessToken, expire);
let refreshTokenExpiresAt = Math.min(expire * 7, 60 * 60 * 24 * 30, 60 * 60 * 24 * 365); // 最大为一年
let refreshTokenExpiresIn = Math.min(expire * 7, 60 * 60 * 24 * 30, 60 * 60 * 24 * 365); // 最大为一年
if (refreshToken) {
// 小于7天, 则设置为7天
if (refreshTokenExpiresAt < 60 * 60 * 24 * 7) {
refreshTokenExpiresAt = 60 * 60 * 24 * 7;
if (refreshTokenExpiresIn < 60 * 60 * 24 * 7) {
refreshTokenExpiresIn = 60 * 60 * 24 * 7;
}
await this.set(refreshToken, JSON.stringify(value), refreshTokenExpiresAt);
await this.set(userPrefix + ':refreshToken:' + refreshToken, refreshToken, refreshTokenExpiresAt);
await this.set(refreshToken, JSON.stringify(value), refreshTokenExpiresIn);
await this.set(userPrefix + ':refreshToken:' + refreshToken, refreshToken, refreshTokenExpiresIn);
}
return {
accessToken,
accessTokenExpiresAt: expire,
accessTokenExpiresIn: expire,
refreshToken,
refreshTokenExpiresAt,
refreshTokenExpiresIn: refreshTokenExpiresIn,
}
}
async delKeys(keys: string[]) {

View File

@@ -11,7 +11,6 @@ export const getTextContentType = (filePath: string, isFilePath = false) => {
'.env',
'.example',
'.log',
'.mjs',
'.map',
'.json5',
'.pem',

View File

@@ -3,11 +3,11 @@ export const createStudioAppListHtml = (opts: StudioOpts) => {
const user = opts.user!;
const userAppKey = opts?.userAppKey;
let showUserAppKey = userAppKey;
if (showUserAppKey && showUserAppKey.startsWith(user + '-')) {
showUserAppKey = showUserAppKey.replace(user + '-', '');
if (showUserAppKey && showUserAppKey.startsWith(user + '--')) {
showUserAppKey = showUserAppKey.replace(user + '--', '');
}
const pathApps = opts?.appIds?.map(appId => {
const shortAppId = appId.replace(opts!.user + '-', '')
const shortAppId = appId.replace(opts!.user + '--', '')
return {
appId,
shortAppId,

View File

@@ -20,8 +20,14 @@ export const wssFun: WebSocketListenerFun = async (req, res) => {
return;
}
const user = loginUser?.tokenUser?.username;
const userApp = user + '-' + id;
const userApp = user + '--' + id;
logger.debug('注册 ws 连接', userApp);
const wsMessage = wsProxyManager.get(userApp);
if (wsMessage) {
logger.debug('ws 连接已存在,关闭旧连接', userApp);
wsMessage.ws.close();
wsProxyManager.unregister(userApp);
}
// @ts-ignore
wsProxyManager.register(userApp, { user, ws });
ws.send(

View File

@@ -2,21 +2,51 @@ import { nanoid } from 'nanoid';
import { WebSocket } from 'ws';
import { logger } from '../logger.ts';
import { EventEmitter } from 'eventemitter3';
import { set } from 'zod';
class WsMessage {
ws: WebSocket;
user?: string;
emitter: EventEmitter;;
emitter: EventEmitter;
private pingTimer?: NodeJS.Timeout;
private readonly PING_INTERVAL = 30000; // 30 秒发送一次 ping
constructor({ ws, user }: WssMessageOptions) {
this.ws = ws;
this.user = user;
this.emitter = new EventEmitter();
this.startPing();
}
private startPing() {
this.stopPing();
this.pingTimer = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.ping();
} else {
this.stopPing();
}
}, this.PING_INTERVAL);
}
private stopPing() {
if (this.pingTimer) {
clearInterval(this.pingTimer);
this.pingTimer = undefined;
}
}
destroy() {
this.stopPing();
this.emitter.removeAllListeners();
}
async sendResponse(data: any) {
if (data.id) {
this.emitter.emit(data.id, data?.data);
}
}
async sendData(data: any, opts?: { timeout?: number }) {
async sendData(data: any, context?: any, opts?: { timeout?: number }) {
if (this.ws.readyState !== WebSocket.OPEN) {
return { code: 500, message: 'WebSocket is not open' };
}
@@ -25,7 +55,10 @@ class WsMessage {
const message = JSON.stringify({
id,
type: 'proxy',
data,
data: {
message: data,
context: context || {},
},
});
logger.info('ws-proxy sendData', message);
this.ws.send(message);
@@ -50,15 +83,22 @@ type WssMessageOptions = {
};
export class WsProxyManager {
wssMap: Map<string, WsMessage> = new Map();
constructor() { }
PING_INTERVAL = 30000; // 30 秒检查一次连接状态
constructor(opts?: { pingInterval?: number }) {
if (opts?.pingInterval) {
this.PING_INTERVAL = opts.pingInterval;
}
this.checkConnceted();
}
register(id: string, opts?: { ws: WebSocket; user: string }) {
if (this.wssMap.has(id)) {
const value = this.wssMap.get(id);
if (value) {
value.ws.close();
value.destroy();
}
}
const [username, appId] = id.split('-');
const [username, appId] = id.split('--');
const url = new URL(`/${username}/v1/${appId}`, 'https://kevisual.cn/');
console.log('WsProxyManager register', id, '访问地址', url.toString());
const value = new WsMessage({ ws: opts?.ws, user: opts?.user });
@@ -68,6 +108,7 @@ export class WsProxyManager {
const value = this.wssMap.get(id);
if (value) {
value.ws.close();
value.destroy();
}
this.wssMap.delete(id);
}
@@ -80,4 +121,16 @@ export class WsProxyManager {
get(id: string) {
return this.wssMap.get(id);
}
checkConnceted() {
const that = this;
setTimeout(() => {
that.wssMap.forEach((value, key) => {
if (value.ws.readyState !== WebSocket.OPEN) {
logger.debug('ws not connected, unregister', key);
that.unregister(key);
}
});
that.checkConnceted();
}, this.PING_INTERVAL);
}
}

View File

@@ -5,6 +5,7 @@ import { App } from '@kevisual/router';
import { logger } from '../logger.ts';
import { getLoginUser } from '@/modules/auth.ts';
import { createStudioAppListHtml } from '../html/studio-app-list/index.ts';
import { omit } from 'es-toolkit';
type ProxyOptions = {
createNotFoundPage: (msg?: string) => any;
@@ -31,7 +32,7 @@ export const UserV1Proxy = async (req: IncomingMessage, res: ServerResponse, opt
if (!userAppKey) {
if (isAdmin) {
// 获取所有的管理员的应用列表
const ids = wsProxyManager.getIds(user + '-');
const ids = wsProxyManager.getIds(user + '--');
const html = createStudioAppListHtml({ user, appIds: ids, userAppKey });
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(html);
@@ -41,8 +42,8 @@ export const UserV1Proxy = async (req: IncomingMessage, res: ServerResponse, opt
return false;
}
}
if (!userAppKey.includes('-')) {
userAppKey = user + '-' + userAppKey;
if (!userAppKey.includes('--')) {
userAppKey = user + '--' + userAppKey;
}
// TODO: 如果不是管理员,是否需要添加其他人可以访问的逻辑?
@@ -50,12 +51,12 @@ export const UserV1Proxy = async (req: IncomingMessage, res: ServerResponse, opt
opts?.createNotFoundPage?.('没有访问应用权限');
return false;
}
if (!userAppKey.startsWith(user + '-')) {
userAppKey = user + '-' + userAppKey;
if (!userAppKey.startsWith(user + '--')) {
userAppKey = user + '--' + userAppKey;
}
logger.debug('data', data);
const client = wsProxyManager.get(userAppKey);
const ids = wsProxyManager.getIds(user + '-');
const ids = wsProxyManager.getIds(user + '--');
if (!client) {
if (isAdmin) {
const html = createStudioAppListHtml({ user, appIds: ids, userAppKey });
@@ -74,7 +75,13 @@ export const UserV1Proxy = async (req: IncomingMessage, res: ServerResponse, opt
res.end(await html);
return true;
}
const value = await client.sendData(data);
let message: any = data;
if (!isAdmin) {
message = omit(data, ['token', 'cookies']);
}
const value = await client.sendData(message, {
state: { tokenUser: omit(loginUser.tokenUser, ['oauthExpand']) },
});
if (value) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(value));

View File

@@ -6,6 +6,7 @@ import { getUidByUsername, prefixFix } from './util.ts';
import { deleteFiles, getMinioListAndSetToAppList } from '../file/index.ts';
import { setExpire } from './revoke.ts';
import { User } from '@/models/user.ts';
import { callDetectAppVersion } from './export.ts';
app
.route({
path: 'app',
@@ -258,7 +259,7 @@ app
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id, username, appKey, version } = ctx.query.data;
const { id, username, appKey, version, detect } = ctx.query.data;
if (!id && !appKey) {
throw new CustomError('id or appKey is required');
}
@@ -268,22 +269,33 @@ app
if (id) {
appList = await AppListModel.findByPk(id);
if (appList?.uid !== uid) {
throw new CustomError('no permission');
ctx.throw('no permission');
}
}
if (!appList && appKey) {
if (!version) {
throw new CustomError('version is required');
ctx.throw('version is required');
}
appList = await AppListModel.findOne({ where: { key: appKey, version, uid } });
}
if (!appList) {
throw new CustomError('app not found');
ctx.throw('app 未发现');
}
if (detect) {
const appKey = appList.key;
const version = appList.version;
// 自动检测最新版本
const res = await callDetectAppVersion({ appKey, version, username: username || tokenUser.username }, ctx.query.token);
if (res.code !== 200) {
ctx.throw(res.message || '检测版本列表失败');
}
appList = await AppListModel.findByPk(appList.id);
}
const files = appList.data.files || [];
const am = await AppModel.findOne({ where: { key: appList.key, uid: uid } });
if (!am) {
throw new CustomError('app not found');
ctx.throw('app 未发现');
}
await am.update({ data: { ...am.data, files }, version: appList.version });
setExpire(appList.key, am.user);
@@ -385,7 +397,7 @@ app
am = await AppModel.create({
title: appKey,
key: appKey,
version: version || '0.0.0',
version: version || '0.0.1',
user: checkUsername,
uid,
data: { files: needAddFiles },

View File

@@ -1,8 +1,8 @@
import { app } from '@/app.ts';
import { ConfigModel } from './models/model.ts';
import { eq, and, inArray } from 'drizzle-orm';
import { app, db, schema } from '@/app.ts';
import { oss } from '@/app.ts';
import { ConfigOssService } from '@kevisual/oss/services';
import { Op } from 'sequelize';
import { nanoid } from 'nanoid';
app
.route({
@@ -20,14 +20,12 @@ app
},
});
const { list, keys, keyEtagMap } = await configOss.getList();
const configList = await ConfigModel.findAll({
where: {
key: {
[Op.in]: keys,
},
uid: tokenUser.id,
},
});
const configList = await db.select()
.from(schema.kvConfig)
.where(and(
inArray(schema.kvConfig.key, keys),
eq(schema.kvConfig.uid, tokenUser.id)
));
const needUpdateList = list.filter((item) => {
const key = item.key;
const hash = keyEtagMap.get(key);
@@ -43,30 +41,33 @@ app
const keyETag = keyEtagMap.get(key);
const configData = keyDataMap.get(key);
if (keyETag && configData) {
const [config, created] = await ConfigModel.findOrCreate({
where: {
key,
uid: tokenUser.id,
},
defaults: {
const existing = await db.select()
.from(schema.kvConfig)
.where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, tokenUser.id)))
.limit(1);
let config;
if (existing.length === 0) {
const inserted = await db.insert(schema.kvConfig).values({
id: nanoid(),
key,
title: key,
description: `${key}:${keyETag} 同步而来`,
uid: tokenUser.id,
hash: keyETag,
data: configData,
},
});
if (!created) {
await config.update(
{
}).returning();
config = inserted[0];
} else {
const updated = await db.update(schema.kvConfig)
.set({
hash: keyETag,
data: json,
},
{
fields: ['hash', 'data'],
},
);
updatedAt: new Date().toISOString(),
})
.where(eq(schema.kvConfig.id, existing[0].id))
.returning();
config = updated[0];
}
updateList.push(config);
}

View File

@@ -1,7 +1,8 @@
import { app } from '@/app.ts';
import { ConfigModel } from './models/model.ts';
import { eq, and } from 'drizzle-orm';
import { app, db, schema } from '@/app.ts';
import { User } from '@/models/user.ts';
import { defaultKeys } from './models/default-keys.ts';
import { nanoid } from 'nanoid';
app
.route({
@@ -27,19 +28,28 @@ app
}
const defaultConfig = defaultKeys.find((item) => item.key === configKey);
const [config, created] = await ConfigModel.findOrCreate({
where: {
key: configKey,
uid: tokenUser.id,
},
defaults: {
const existing = await db.select()
.from(schema.kvConfig)
.where(and(
eq(schema.kvConfig.key, configKey),
eq(schema.kvConfig.uid, tokenUser.id)
))
.limit(1);
let config;
if (existing.length === 0) {
const inserted = await db.insert(schema.kvConfig).values({
id: nanoid(),
title: defaultConfig?.key,
description: defaultConfig?.description || '',
key: configKey,
uid: tokenUser.id,
data: defaultConfig?.data,
},
});
}).returning();
config = inserted[0];
} else {
config = existing[0];
}
ctx.body = config;
})

View File

@@ -1,8 +1,9 @@
import { app } from '@/app.ts';
import { ConfigModel } from './models/model.ts';
import { eq, desc, and, inArray } from 'drizzle-orm';
import { app, db, schema } from '@/app.ts';
import { ShareConfigService } from './services/share.ts';
import { oss } from '@/app.ts';
import { ConfigOssService } from '@kevisual/oss/services';
import { nanoid } from 'nanoid';
app
.route({
@@ -13,12 +14,10 @@ app
})
.define(async (ctx) => {
const { id } = ctx.state.tokenUser;
const config = await ConfigModel.findAll({
where: {
uid: id,
},
order: [['updatedAt', 'DESC']],
});
const config = await db.select()
.from(schema.kvConfig)
.where(eq(schema.kvConfig.uid, id))
.orderBy(desc(schema.kvConfig.updatedAt));
ctx.body = {
list: config,
};
@@ -36,9 +35,10 @@ app
const tokernUser = ctx.state.tokenUser;
const tuid = tokernUser.id;
const { id, data, ...rest } = ctx.query?.data || {};
let config: ConfigModel;
let config: any;
if (id) {
config = await ConfigModel.findByPk(id);
const configs = await db.select().from(schema.kvConfig).where(eq(schema.kvConfig.id, id)).limit(1);
config = configs[0];
let keyIsChange = false;
if (rest?.key) {
keyIsChange = rest.key !== config?.key;
@@ -48,47 +48,57 @@ app
}
if (keyIsChange) {
const key = rest.key;
const keyConfig = await ConfigModel.findOne({
where: {
key,
uid: tuid,
},
});
const keyConfigs = await db.select()
.from(schema.kvConfig)
.where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, tuid)))
.limit(1);
const keyConfig = keyConfigs[0];
if (keyConfig && keyConfig.id !== id) {
ctx.throw(403, 'key is already exists');
}
}
await config.update({
const updated = await db.update(schema.kvConfig)
.set({
data: data,
...rest,
});
if (config.data?.permission?.share === 'public') {
updatedAt: new Date().toISOString(),
})
.where(eq(schema.kvConfig.id, id))
.returning();
config = updated[0];
if ((config.data as any)?.permission?.share === 'public') {
await ShareConfigService.expireShareConfig(config.key, tokernUser.username);
}
ctx.body = config;
} else if (rest?.key) {
// id 不存在key存在则属于更新key不能重复
const key = rest.key;
config = await ConfigModel.findOne({
where: {
key,
uid: tuid,
},
});
const configs = await db.select()
.from(schema.kvConfig)
.where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, tuid)))
.limit(1);
config = configs[0];
if (config) {
await config.update({
const updated = await db.update(schema.kvConfig)
.set({
data: data,
...rest,
});
updatedAt: new Date().toISOString(),
})
.where(eq(schema.kvConfig.id, config.id))
.returning();
config = updated[0];
ctx.body = config;
} else {
// 根据key创建一个配置
config = await ConfigModel.create({
const inserted = await db.insert(schema.kvConfig).values({
id: nanoid(),
key,
...rest,
data: data,
uid: tuid,
});
}).returning();
config = inserted[0];
ctx.body = config;
}
}
@@ -103,22 +113,25 @@ app
const data = config.data;
const hash = ossConfig.hash(data);
if (config.hash !== hash) {
config.hash = hash;
await config.save({
fields: ['hash'],
});
await db.update(schema.kvConfig)
.set({
hash: hash,
updatedAt: new Date().toISOString(),
})
.where(eq(schema.kvConfig.id, config.id));
await ossConfig.putJsonObject(key, data);
}
}
if (config) return;
// id和key不存在。创建一个新的配置, 而且没有id的
const newConfig = await ConfigModel.create({
const newConfig = await db.insert(schema.kvConfig).values({
id: nanoid(),
...rest,
data: data,
uid: tuid,
});
ctx.body = newConfig;
}).returning();
ctx.body = newConfig[0];
})
.addTo(app);
@@ -136,17 +149,17 @@ app
if (!id && !key) {
ctx.throw(400, 'id or key is required');
}
let config: ConfigModel;
let config: any;
if (id) {
config = await ConfigModel.findByPk(id);
const configs = await db.select().from(schema.kvConfig).where(eq(schema.kvConfig.id, id)).limit(1);
config = configs[0];
}
if (!config && key) {
config = await ConfigModel.findOne({
where: {
key,
uid: tuid,
},
});
const configs = await db.select()
.from(schema.kvConfig)
.where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, tuid)))
.limit(1);
config = configs[0];
}
if (!config) {
ctx.throw(404, 'config not found');
@@ -171,12 +184,9 @@ app
const tuid = tokernUser.id;
const { id, key } = ctx.query?.data || {};
if (id || key) {
const search: any = id ? { id } : { key };
const config = await ConfigModel.findOne({
where: {
...search
},
});
const search: any = id ? eq(schema.kvConfig.id, id) : eq(schema.kvConfig.key, key);
const configs = await db.select().from(schema.kvConfig).where(search).limit(1);
const config = configs[0];
if (config && config.uid === tuid) {
const key = config.key;
const ossConfig = ConfigOssService.fromBase({
@@ -190,7 +200,7 @@ app
await ossConfig.deleteObject(key);
} catch (e) { }
}
await config.destroy();
await db.delete(schema.kvConfig).where(eq(schema.kvConfig.id, config.id));
} else {
ctx.throw(403, 'no permission');
}

View File

@@ -1,7 +1,8 @@
import { useContextKey } from '@kevisual/context';
import { sequelize } from '../../../modules/sequelize.ts';
import { DataTypes, Model } from 'sequelize';
import { Permission } from '@kevisual/permission';
import { eq, and } from 'drizzle-orm';
import { db, schema } from '../../../app.ts';
import { nanoid } from 'nanoid';
export interface ConfigData {
key?: string;
@@ -9,23 +10,24 @@ export interface ConfigData {
permission?: Permission;
}
export type Config = Partial<InstanceType<typeof ConfigModel>>;
export type Config = {
id: string;
title: string | null;
description: string | null;
tags: unknown;
key: string | null;
data: unknown;
uid: string | null;
hash: string | null;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
};
/**
* 用户配置
*/
export class ConfigModel extends Model {
declare id: string;
declare title: string;
declare description: string;
declare tags: string[];
/**
* @important 配置key 默认可以为空,如何设置了,必须要唯一。
*/
declare key: string;
declare data: ConfigData; // files
declare uid: string;
declare hash: string;
export class ConfigModel {
/**
* 获取用户配置
* @param key 配置key
@@ -35,37 +37,60 @@ export class ConfigModel extends Model {
* @returns 配置
*/
static async getConfig(key: string, opts: { uid: string; defaultData?: any }) {
const [config, isNew] = await ConfigModel.findOrCreate({
where: { key, uid: opts.uid },
defaults: {
const existing = await db.select()
.from(schema.kvConfig)
.where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, opts.uid)))
.limit(1);
if (existing.length > 0) {
return {
config: existing[0],
isNew: false,
};
}
const inserted = await db.insert(schema.kvConfig).values({
id: nanoid(),
key,
title: key,
uid: opts.uid,
data: opts?.defaultData || {},
},
});
}).returning();
return {
config: config,
isNew,
config: inserted[0],
isNew: true,
};
}
static async setConfig(key: string, opts: { uid: string; data: any }) {
let config = await ConfigModel.findOne({
where: { key, uid: opts.uid },
});
if (config) {
config.data = { ...config.data, ...opts.data };
await config.save();
const existing = await db.select()
.from(schema.kvConfig)
.where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, opts.uid)))
.limit(1);
if (existing.length > 0) {
const config = existing[0];
const updated = await db.update(schema.kvConfig)
.set({
data: { ...(config.data as any || {}), ...opts.data },
updatedAt: new Date().toISOString(),
})
.where(eq(schema.kvConfig.id, config.id))
.returning();
return updated[0];
} else {
config = await ConfigModel.create({
const inserted = await db.insert(schema.kvConfig).values({
id: nanoid(),
title: key,
key,
uid: opts.uid,
data: opts.data,
});
}).returning();
return inserted[0];
}
return config;
}
/**
* 获取上传配置
* @param key 配置key
@@ -82,7 +107,7 @@ export class ConfigModel extends Model {
uid: opts.uid,
defaultData: defaultConfig,
});
const data = config.config.data;
const data = config.config.data as any;
const prefix = `/${data.key}/${data.version}`;
return {
config: config.config,
@@ -90,6 +115,7 @@ export class ConfigModel extends Model {
prefix,
};
}
static async setUploadConfig(opts: { uid: string; data: { key?: string; version?: string } }) {
const config = await ConfigModel.setConfig('upload.json', {
uid: opts.uid,
@@ -98,52 +124,5 @@ export class ConfigModel extends Model {
return config;
}
}
ConfigModel.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
comment: 'id',
},
title: {
type: DataTypes.TEXT,
defaultValue: '',
},
key: {
type: DataTypes.TEXT,
defaultValue: '',
},
description: {
type: DataTypes.TEXT,
defaultValue: '',
},
tags: {
type: DataTypes.JSONB,
defaultValue: [],
},
hash: {
type: DataTypes.TEXT,
defaultValue: '',
},
data: {
type: DataTypes.JSONB,
defaultValue: {},
},
uid: {
type: DataTypes.UUID,
allowNull: true,
},
},
{
sequelize,
tableName: 'kv_config',
paranoid: true,
},
);
// ConfigModel.sync({ alter: true, logging: false }).catch((e) => {
// console.error('ConfigModel sync', e);
// });
useContextKey('ConfigModel', () => ConfigModel);

View File

@@ -1,10 +1,10 @@
import { ConfigModel, Config } from '../models/model.ts';
import { Config } from '../models/model.ts';
import { CustomError } from '@kevisual/router';
import { redis } from '@/app.ts';
import { User } from '@/models/user.ts';
import { redis, db, schema } from '@/app.ts';
import { eq, and } from 'drizzle-orm';
import { UserPermission, UserPermissionOptions } from '@kevisual/permission';
export class ShareConfigService extends ConfigModel {
export class ShareConfigService {
/**
* 获取分享的配置
* @param key 配置的key
@@ -22,26 +22,30 @@ export class ShareConfigService extends ConfigModel {
}
const owner = username;
if (shareCacheConfig) {
const permission = new UserPermission({ permission: shareCacheConfig?.data?.permission, owner });
const permission = new UserPermission({ permission: (shareCacheConfig?.data as any)?.permission, owner });
const result = permission.checkPermissionSuccess(options);
if (!result.success) {
throw new CustomError(403, 'no permission');
}
return shareCacheConfig;
}
const user = await User.findOne({
where: { username },
});
const users = await db.select()
.from(schema.cfUser)
.where(eq(schema.cfUser.username, username))
.limit(1);
const user = users[0];
if (!user) {
throw new CustomError(404, 'user not found');
}
const config = await ConfigModel.findOne({
where: { key, uid: user.id },
});
const configs = await db.select()
.from(schema.kvConfig)
.where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, user.id)))
.limit(1);
const config = configs[0];
if (!config) {
throw new CustomError(404, 'config not found');
}
const permission = new UserPermission({ permission: config?.data?.permission, owner });
const permission = new UserPermission({ permission: (config?.data as any)?.permission, owner });
const result = permission.checkPermissionSuccess(options);
if (!result.success) {
throw new CustomError(403, 'no permission');

View File

@@ -13,8 +13,9 @@ app
const config = await ConfigModel.getUploadConfig({
uid: tokenUser.id,
});
const key = config?.config?.data?.key || '';
const version = config?.config?.data?.version || '';
const data: any = config?.config?.data || {};
const key = data.key || '';
const version = data.version || '';
const username = tokenUser.username;
const prefix = `${key}/${version}/`;
ctx.body = {
@@ -35,7 +36,7 @@ app
})
.define(async (ctx) => {
const { id } = ctx.state.tokenUser;
const data = ctx.query.data || {};
const data = ctx.query?.data || {};
const { key, version } = data;
if (!key && !version) {
ctx.throw(400, 'key or version is required');

View File

@@ -10,6 +10,8 @@ import './config/index.ts';
// import './file-listener/index.ts';
import './mark/index.ts';
import './light-code/index.ts';
import './ai/index.ts';

View File

@@ -1,7 +1,8 @@
import { eq, desc, and, like, or } from 'drizzle-orm';
import { CustomError } from '@kevisual/router';
import { app, db, schema } from '../../app.ts';
import { CustomError } from '@kevisual/router';
import { filter } from '@kevisual/js-filter'
import { z } from 'zod';
app
.route({
path: 'light-code',
@@ -9,10 +10,22 @@ app
description: `获取轻代码列表,参数
type: 代码类型light-code, ts`,
middleware: ['auth'],
metadata: {
args: {
type: z.string().optional().describe('代码类型light-code, ts'),
search: z.string().optional().describe('搜索关键词,匹配标题和描述'),
filter: z
.string()
.optional()
.describe(
'过滤条件SQL like格式字符串例如WHERE tags LIKE \'%tag1%\' AND tags LIKE \'%tag2%\'',
),
}
}
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { type, search } = ctx.query || {};
const { type, search, filter: filterQuery } = ctx.query || {};
const conditions = [eq(schema.kvContainer.uid, tokenUser.id)];
if (type) {
conditions.push(eq(schema.kvContainer.type, type as string));
@@ -43,7 +56,12 @@ app
.from(schema.kvContainer)
.where(and(...conditions))
.orderBy(desc(schema.kvContainer.updatedAt));
if (filterQuery) {
const filteredList = filter(list, filterQuery);
ctx.body = { list: filteredList }
} else {
ctx.body = { list };
}
return ctx;
})
.addTo(app);
@@ -140,7 +158,7 @@ app
app
.route({
path: 'container',
path: 'light-code',
key: 'delete',
middleware: ['auth'],
})

1
src/routes/mark/index.ts Normal file
View File

@@ -0,0 +1 @@
import './list.ts';

308
src/routes/mark/list.ts Normal file
View File

@@ -0,0 +1,308 @@
import { eq, desc, and, like, or, count, sql } from 'drizzle-orm';
import { app, db, schema } from '../../app.ts';
import { MarkServices } from './services/mark.ts';
import dayjs from 'dayjs';
import { nanoid } from 'nanoid';
app
.route({
path: 'mark',
key: 'list',
description: 'mark list.',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
ctx.body = await MarkServices.getList({
uid: tokenUser.id,
query: ctx.query,
queryType: 'simple',
});
})
.addTo(app);
app
.route({
path: 'mark',
key: 'getVersion',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id } = ctx.query;
if (id) {
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
const markModel = marks[0];
if (!markModel) {
ctx.throw(404, 'mark not found');
}
if (markModel.uid !== tokenUser.id) {
ctx.throw(403, 'no permission');
}
ctx.body = {
version: Number(markModel.version),
updatedAt: markModel.updatedAt,
createdAt: markModel.createdAt,
id: markModel.id,
};
} else {
ctx.throw(400, 'id is required');
// const [markModel, created] = await MarkModel.findOrCreate({
// where: {
// uid: tokenUser.id,
// puid: tokenUser.uid,
// title: dayjs().format('YYYY-MM-DD'),
// },
// defaults: {
// title: dayjs().format('YYYY-MM-DD'),
// uid: tokenUser.id,
// markType: 'wallnote',
// tags: ['daily'],
// },
// });
// ctx.body = {
// version: Number(markModel.version),
// updatedAt: markModel.updatedAt,
// createdAt: markModel.createdAt,
// id: markModel.id,
// created: created,
// };
}
})
.addTo(app);
app
.route({
path: 'mark',
key: 'get',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id } = ctx.query;
if (id) {
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
const markModel = marks[0];
if (!markModel) {
ctx.throw(404, 'mark not found');
}
if (markModel.uid !== tokenUser.id) {
ctx.throw(403, 'no permission');
}
ctx.body = markModel;
} else {
ctx.throw(400, 'id is required');
// id 不存在获取当天的title为 日期的一条数据
// const [markModel, created] = await MarkModel.findOrCreate({
// where: {
// uid: tokenUser.id,
// puid: tokenUser.uid,
// title: dayjs().format('YYYY-MM-DD'),
// },
// defaults: {
// title: dayjs().format('YYYY-MM-DD'),
// uid: tokenUser.id,
// markType: 'wallnote',
// tags: ['daily'],
// uname: tokenUser.username,
// puid: tokenUser.uid,
// version: 1,
// },
// });
// ctx.body = markModel;
}
})
.addTo(app);
app
.route({
path: 'mark',
key: 'update',
middleware: ['auth'],
isDebug: true,
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id, createdAt, updatedAt, uid: _, puid: _2, uname: _3, data, ...rest } = ctx.query.data || {};
let markModel: any;
if (id) {
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
markModel = marks[0];
if (!markModel) {
ctx.throw(404, 'mark not found');
}
if (markModel.uid !== tokenUser.id) {
ctx.throw(403, 'no permission');
}
const version = Number(markModel.version) + 1;
const updated = await db.update(schema.microMark)
.set({
...rest,
data: {
...(markModel.data as any || {}),
...data,
},
version,
updatedAt: new Date().toISOString(),
})
.where(eq(schema.microMark.id, id))
.returning();
markModel = updated[0];
} else {
const inserted = await db.insert(schema.microMark).values({
id: nanoid(),
data: data || {},
...rest,
uname: tokenUser.username,
uid: tokenUser.id,
puid: tokenUser.uid,
}).returning();
markModel = inserted[0];
}
ctx.body = markModel;
})
.addTo(app);
app
.route({
path: 'mark',
key: 'updateNode',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const operate = ctx.query.operate || 'update';
const { id, node } = ctx.query.data || {};
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
const markModel = marks[0];
if (!markModel) {
ctx.throw(404, 'mark not found');
}
if (markModel.uid !== tokenUser.id) {
ctx.throw(403, 'no permission');
}
// Update JSON node logic with Drizzle
const currentData = markModel.data as any || {};
const nodes = currentData.nodes || [];
const nodeIndex = nodes.findIndex((n: any) => n.id === node.id);
let updatedNodes;
if (operate === 'delete') {
updatedNodes = nodes.filter((n: any) => n.id !== node.id);
} else if (nodeIndex >= 0) {
updatedNodes = [...nodes];
updatedNodes[nodeIndex] = { ...nodes[nodeIndex], ...node };
} else {
updatedNodes = [...nodes, node];
}
const version = Number(markModel.version) + 1;
const updated = await db.update(schema.microMark)
.set({
data: { ...currentData, nodes: updatedNodes },
version,
updatedAt: new Date().toISOString(),
})
.where(eq(schema.microMark.id, id))
.returning();
ctx.body = updated[0];
})
.addTo(app);
app
.route({
path: 'mark',
key: 'updateNodes',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id, nodeOperateList } = ctx.query.data || {};
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
const markModel = marks[0];
if (!markModel) {
ctx.throw(404, 'mark not found');
}
if (markModel.uid !== tokenUser.id) {
ctx.throw(403, 'no permission');
}
if (!nodeOperateList || !Array.isArray(nodeOperateList) || nodeOperateList.length === 0) {
ctx.throw(400, 'nodeOperateList is required');
}
if (nodeOperateList.some((item: any) => !item.node)) {
ctx.throw(400, 'nodeOperateList node is required');
}
// Update multiple JSON nodes logic with Drizzle
const currentData = markModel.data as any || {};
let nodes = currentData.nodes || [];
for (const item of nodeOperateList) {
const { node, operate = 'update' } = item;
const nodeIndex = nodes.findIndex((n: any) => n.id === node.id);
if (operate === 'delete') {
nodes = nodes.filter((n: any) => n.id !== node.id);
} else if (nodeIndex >= 0) {
nodes[nodeIndex] = { ...nodes[nodeIndex], ...node };
} else {
nodes.push(node);
}
}
const version = Number(markModel.version) + 1;
const updated = await db.update(schema.microMark)
.set({
data: { ...currentData, nodes },
version,
updatedAt: new Date().toISOString(),
})
.where(eq(schema.microMark.id, id))
.returning();
ctx.body = updated[0];
})
.addTo(app);
app
.route({
path: 'mark',
key: 'delete',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id } = ctx.query;
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
const markModel = marks[0];
if (!markModel) {
ctx.throw(404, 'mark not found');
}
if (markModel.uid !== tokenUser.id) {
ctx.throw(403, 'no permission');
}
await db.delete(schema.microMark).where(eq(schema.microMark.id, id));
ctx.body = markModel;
})
.addTo(app);
app
.route({ path: 'mark', key: 'getMenu', description: '获取菜单', middleware: ['auth'] })
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const [rows, totalResult] = await Promise.all([
db.select({
id: schema.microMark.id,
title: schema.microMark.title,
summary: schema.microMark.summary,
tags: schema.microMark.tags,
thumbnail: schema.microMark.thumbnail,
link: schema.microMark.link,
createdAt: schema.microMark.createdAt,
updatedAt: schema.microMark.updatedAt,
}).from(schema.microMark).where(eq(schema.microMark.uid, tokenUser.id)),
db.select({ count: count() }).from(schema.microMark).where(eq(schema.microMark.uid, tokenUser.id))
]);
ctx.body = {
list: rows,
total: totalResult[0]?.count || 0,
};
})
.addTo(app);

View File

@@ -0,0 +1,327 @@
import { useContextKey } from '@kevisual/context';
import { nanoid, customAlphabet } from 'nanoid';
import { DataTypes, Model, ModelAttributes } from 'sequelize';
import type { Sequelize } from 'sequelize';
export const random = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
export type Mark = Partial<InstanceType<typeof MarkModel>>;
export type MarkData = {
md?: string; // markdown
mdList?: string[]; // markdown list
type?: string; // 类型 markdown | json | html | image | video | audio | code | link | file
data?: any;
key?: string; // 文件的名称, 唯一
push?: boolean; // 是否推送到elasticsearch
pushTime?: Date; // 推送时间
summary?: string; // 摘要
nodes?: MarkDataNode[]; // 节点
[key: string]: any;
};
export type MarkFile = {
id: string;
name: string;
url: string;
size: number;
type: 'self' | 'data' | 'generate'; // generate为生成文件
query: string; // 'data.nodes[id].content';
hash: string;
fileKey: string; // 文件的名称, 唯一
};
export type MarkDataNode = {
id?: string;
[key: string]: any;
};
export type MarkConfig = {
[key: string]: any;
};
export type MarkAuth = {
[key: string]: any;
};
/**
* 隐秘内容
* auth
* config
*
*/
export class MarkModel extends Model {
declare id: string;
declare title: string; // 标题可以ai生成
declare description: string; // 描述可以ai生成
declare cover: string; // 封面可以ai生成
declare thumbnail: string; // 缩略图
declare key: string; // 文件路径
declare markType: string; // markdown | json | html | image | video | audio | code | link | file
declare link: string; // 访问链接
declare tags: string[]; // 标签
declare summary: string; // 摘要, description的简化版
declare data: MarkData; // 数据
declare uid: string; // 操作用户的id
declare puid: string; // 父级用户的id, 真实用户
declare config: MarkConfig; // mark属于一定不会暴露的内容。
declare fileList: MarkFile[]; // 文件管理
declare uname: string; // 用户的名称, 或者着别名
declare markedAt: Date; // 标记时间
declare createdAt: Date;
declare updatedAt: Date;
declare version: number;
/**
* 加锁更新data中的node的节点通过node的id
* @param param0
*/
static async updateJsonNode(id: string, node: MarkDataNode, opts?: { operate?: 'update' | 'delete'; Model?: any; sequelize?: Sequelize }) {
const sequelize = opts?.sequelize || (await useContextKey('sequelize'));
const transaction = await sequelize.transaction(); // 开启事务
const operate = opts.operate || 'update';
const isUpdate = operate === 'update';
const Model = opts.Model || MarkModel;
try {
// 1. 获取当前的 JSONB 字段值(加锁)
const mark = await Model.findByPk(id, {
transaction,
lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改
});
if (!mark) {
throw new Error('Mark not found');
}
// 2. 修改特定的数组元素
const data = mark.data as MarkData;
const items = data.nodes;
if (!node.id) {
node.id = random(12);
}
// 找到要更新的元素
const itemIndex = items.findIndex((item) => item.id === node.id);
if (itemIndex === -1) {
isUpdate && items.push(node);
} else {
if (isUpdate) {
items[itemIndex] = node;
} else {
items.splice(itemIndex, 1);
}
}
const version = Number(mark.version) + 1;
// 4. 更新 JSONB 字段
const result = await mark.update(
{
data: {
...data,
nodes: items,
},
version,
},
{ transaction },
);
await transaction.commit();
return result;
} catch (error) {
await transaction.rollback();
throw error;
}
}
static async updateJsonNodes(id: string, nodes: { node: MarkDataNode; operate?: 'update' | 'delete' }[], opts?: { Model?: any; sequelize?: Sequelize }) {
const sequelize = opts?.sequelize || (await useContextKey('sequelize'));
const transaction = await sequelize.transaction(); // 开启事务
const Model = opts?.Model || MarkModel;
try {
const mark = await Model.findByPk(id, {
transaction,
lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改
});
if (!mark) {
throw new Error('Mark not found');
}
const data = mark.data as MarkData;
const _nodes = data.nodes || [];
// 过滤不在nodes中的节点
const blankNodes = nodes.filter((node) => !_nodes.find((n) => n.id === node.node.id)).map((node) => node.node);
// 更新或删除节点
const newNodes = _nodes
.map((node) => {
const nodeOperate = nodes.find((n) => n.node.id === node.id);
if (nodeOperate) {
if (nodeOperate.operate === 'delete') {
return null;
}
return nodeOperate.node;
}
return node;
})
.filter((node) => node !== null);
const version = Number(mark.version) + 1;
const result = await mark.update(
{
data: {
...data,
nodes: [...blankNodes, ...newNodes],
},
version,
},
{ transaction },
);
await transaction.commit();
return result;
} catch (error) {
await transaction.rollback();
throw error;
}
}
static async updateData(id: string, data: MarkData, opts: { Model?: any; sequelize?: Sequelize }) {
const sequelize = opts.sequelize || (await useContextKey('sequelize'));
const transaction = await sequelize.transaction(); // 开启事务
const Model = opts.Model || MarkModel;
const mark = await Model.findByPk(id, {
transaction,
lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改
});
if (!mark) {
throw new Error('Mark not found');
}
const version = Number(mark.version) + 1;
const result = await mark.update(
{
...mark.data,
...data,
data: {
...mark.data,
...data,
},
version,
},
{ transaction },
);
await transaction.commit();
return result;
}
static async createNew(data: any, opts: { Model?: any; sequelize?: Sequelize }) {
const sequelize = opts.sequelize || (await useContextKey('sequelize'));
const transaction = await sequelize.transaction(); // 开启事务
const Model = opts.Model || MarkModel;
const result = await Model.create({ ...data, version: 1 }, { transaction });
await transaction.commit();
return result;
}
}
export type MarkInitOpts<T = any> = {
tableName: string;
sequelize?: Sequelize;
callInit?: (attribute: ModelAttributes) => ModelAttributes;
Model?: T extends typeof MarkModel ? T : typeof MarkModel;
};
export type Opts = {
sync?: boolean;
alter?: boolean;
logging?: boolean | ((...args: any) => any);
force?: boolean;
};
export const MarkMInit = async <T = any>(opts: MarkInitOpts<T>, sync?: Opts) => {
const sequelize = await useContextKey('sequelize');
opts.sequelize = opts.sequelize || sequelize;
const { callInit, Model, ...optsRest } = opts;
const modelAttribute = {
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
comment: 'id',
},
title: {
type: DataTypes.TEXT,
defaultValue: '',
},
key: {
type: DataTypes.TEXT, // 对应的minio的文件路径
defaultValue: '',
},
markType: {
type: DataTypes.TEXT,
defaultValue: 'md', // markdown | json | html | image | video | audio | code | link | file
comment: '类型',
},
description: {
type: DataTypes.TEXT,
defaultValue: '',
},
cover: {
type: DataTypes.TEXT,
defaultValue: '',
comment: '封面',
},
thumbnail: {
type: DataTypes.TEXT,
defaultValue: '',
comment: '缩略图',
},
link: {
type: DataTypes.TEXT,
defaultValue: '',
comment: '链接',
},
tags: {
type: DataTypes.JSONB,
defaultValue: [],
},
summary: {
type: DataTypes.TEXT,
defaultValue: '',
comment: '摘要',
},
config: {
type: DataTypes.JSONB,
defaultValue: {},
},
data: {
type: DataTypes.JSONB,
defaultValue: {},
},
fileList: {
type: DataTypes.JSONB,
defaultValue: [],
},
uname: {
type: DataTypes.STRING,
defaultValue: '',
comment: '用户的名称, 更新后的用户的名称',
},
version: {
type: DataTypes.INTEGER, // 更新刷新版本,多人协作
defaultValue: 1,
},
markedAt: {
type: DataTypes.DATE,
allowNull: true,
comment: '标记时间',
},
uid: {
type: DataTypes.UUID,
allowNull: true,
},
puid: {
type: DataTypes.UUID,
allowNull: true,
},
};
const InitModel = Model || MarkModel;
InitModel.init(callInit ? callInit(modelAttribute) : modelAttribute, {
sequelize,
paranoid: true,
...optsRest,
});
if (sync && sync.sync) {
const { sync: _, ...rest } = sync;
MarkModel.sync({ alter: true, logging: false, ...rest }).catch((e) => {
console.error('MarkModel sync', e);
});
}
};
export const markModelInit = MarkMInit;
export const syncMarkModel = async (sync?: Opts, tableName = 'micro_mark') => {
const sequelize = await useContextKey('sequelize');
await MarkMInit({ sequelize, tableName }, sync);
};

5
src/routes/mark/model.ts Normal file
View File

@@ -0,0 +1,5 @@
export * from '@kevisual/code-center-module/src/mark/mark-model.ts';
import { markModelInit, MarkModel, syncMarkModel } from '@kevisual/code-center-module/src/mark/mark-model.ts';
export { markModelInit, MarkModel };
syncMarkModel({ sync: true, alter: true, logging: false });

View File

@@ -0,0 +1,85 @@
import { eq, desc, asc, and, like, or, count } from 'drizzle-orm';
import { app, db, schema } from '../../../app.ts';
export class MarkServices {
static getList = async (opts: {
/** 查询用户的 */
uid?: string;
query?: {
page?: number;
pageSize?: number;
search?: string;
markType?: string;
sort?: string;
};
/**
* 查询类型
* simple: 简单查询 默认
*/
queryType?: string;
}) => {
const { uid, query = {} } = opts;
const { page = 1, pageSize = 999, search, sort = 'DESC' } = query;
const conditions = [];
if (uid) {
conditions.push(eq(schema.microMark.uid, uid));
}
if (search) {
conditions.push(
or(
like(schema.microMark.title, `%${search}%`),
like(schema.microMark.summary, `%${search}%`)
)
);
}
if (opts.query?.markType) {
conditions.push(eq(schema.microMark.markType, opts.query.markType));
}
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
const queryType = opts.queryType || 'simple';
let selectFields: any = {};
if (queryType === 'simple') {
// Exclude data, config, cover, description
selectFields = {
id: schema.microMark.id,
title: schema.microMark.title,
tags: schema.microMark.tags,
uname: schema.microMark.uname,
uid: schema.microMark.uid,
createdAt: schema.microMark.createdAt,
updatedAt: schema.microMark.updatedAt,
thumbnail: schema.microMark.thumbnail,
link: schema.microMark.link,
summary: schema.microMark.summary,
markType: schema.microMark.markType,
puid: schema.microMark.puid,
deletedAt: schema.microMark.deletedAt,
version: schema.microMark.version,
fileList: schema.microMark.fileList,
key: schema.microMark.key,
};
}
const orderByField = sort === 'ASC' ? asc(schema.microMark.updatedAt) : desc(schema.microMark.updatedAt);
const [rows, totalResult] = await Promise.all([
queryType === 'simple'
? db.select(selectFields).from(schema.microMark).where(whereClause).orderBy(orderByField).limit(pageSize).offset((page - 1) * pageSize)
: db.select().from(schema.microMark).where(whereClause).orderBy(orderByField).limit(pageSize).offset((page - 1) * pageSize),
db.select({ count: count() }).from(schema.microMark).where(whereClause)
]);
return {
pagination: {
current: page,
pageSize,
total: totalResult[0]?.count || 0,
},
list: rows,
};
};
}