init6
This commit is contained in:
99
.gitignore
vendored
Normal file
99
.gitignore
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
/FEISHU_TOKEN_IMPLEMENTATION_REPORT.md
|
||||
/build-and-deploy.bat
|
||||
/build-and-deploy.sh
|
||||
/CRUD_EXAMPLE_README.md
|
||||
/DEPLOYMENT_ENV_GUIDE.md
|
||||
/ENV_CONFIG_GUIDE.md
|
||||
/SUMMARY.md
|
||||
/测试文件准备指南.md
|
||||
/技术实现总结.md
|
||||
/双模式加载系统使用说明.md
|
||||
/PROJECT_STRUCTURE.md
|
||||
/QUICKSTART.md
|
||||
/README.md
|
||||
/app/node_modules
|
||||
/app/.nuxt
|
||||
/app/.output
|
||||
/app/public/3DModels
|
||||
/app/components/PanelTemple.vue
|
||||
/app/components/3d
|
||||
/app/components/business
|
||||
/app/components/CIM
|
||||
/app/components/composite
|
||||
/app/components/ui
|
||||
/app/composables/3d-engine
|
||||
/app/composables/use3DScene.ts
|
||||
/app/composables/useCozeAPI.ts
|
||||
/app/composables/useCozeOAuth.ts
|
||||
/app/composables/useFeishu.ts
|
||||
/app/composables/usePowerStationMockData.ts
|
||||
/app/composables/useUnreal.ts
|
||||
/app/composables/useValidation.ts
|
||||
/app/config
|
||||
/app/public/cim
|
||||
/app/public/robots.txt
|
||||
/app/public/images
|
||||
/app/public/dark_china_map.png
|
||||
/app/stores/cimMenu.ts
|
||||
/app/types/cim.ts
|
||||
/app/types/coze.ts
|
||||
/app/utils/token-utils.ts
|
||||
/app/pages/FeiShu/feishu-bitable-crud.vue
|
||||
/app/pages/FeiShu/feishu-token-demo.vue
|
||||
/app/pages/FeiShu/feishu-token-monitor.vue
|
||||
/app/pages/MegaObjsView/ControlsManager.js
|
||||
/app/pages/MegaObjsView/SceneManager.js
|
||||
/app/pages/MegaObjsView/TreeManager.js
|
||||
/app/pages/MegaObjsView/UIManager.js
|
||||
/app/pages/MegaObjsView/index.vue
|
||||
/app/pages/NineSliceTest.vue
|
||||
/app/pages/PanelPages/Page1.vue
|
||||
/app/pages/PanelPages/Page2.vue
|
||||
/app/pages/Panels/Panel1.vue
|
||||
/app/pages/Panels/PanelBarChart.vue
|
||||
/app/pages/Panels/PanelBarChartEcharts.vue
|
||||
/app/pages/Panels/PanelLineChart.vue
|
||||
/app/pages/Panels/PanelLineChartEcharts.vue
|
||||
/app/pages/Panels/PanelMap3DEffect.vue
|
||||
/app/pages/Panels/PanelPieChart.vue
|
||||
/app/pages/Panels/PanelPieChartEcharts.vue
|
||||
/app/pages/Panels/PanelRadarChart.vue
|
||||
/app/pages/Panels/PanelRadarChartEcharts.vue
|
||||
/app/pages/Panels/PanelScrollTable.vue
|
||||
/app/pages/UE5/Info.vue
|
||||
/app/pages/UE5/UE.vue
|
||||
/app/pages/UEProject/areaInfos.vue
|
||||
/app/pages/UEProject/party-members.vue
|
||||
/app/pages/callback.vue
|
||||
/app/pages/coze-workflow-test.vue
|
||||
/app/pages/dashboard-demo.vue
|
||||
/app/pages/dashboard-large-screen.vue
|
||||
/app/pages/datav-vue3-demo.vue
|
||||
/app/pages/hospital-overview.vue
|
||||
/app/pages/3d-browser.vue
|
||||
/app/pages/CIMMenu/index.vue
|
||||
/app/pages/CIMMenu/提示词/设计需求.md
|
||||
/app/pages/oauth/error.vue
|
||||
/app/pages/oauth/exchange-code-test.vue
|
||||
/app/pages/oauth/login.vue
|
||||
/app/pages/oauth/session-test.vue
|
||||
/app/pages/oauth/token-refresh-test.vue
|
||||
/app/pages/posts.vue
|
||||
/app/pages/test-file-loading.md
|
||||
/scripts
|
||||
/shared/node_modules
|
||||
/server/node_modules
|
||||
/server/api/test
|
||||
/server/api/CIMapi
|
||||
/server/api/coze
|
||||
/server/api/feishu
|
||||
/server/api/oauth
|
||||
/server/services
|
||||
/.nuxt
|
||||
/.svn
|
||||
/node_modules
|
||||
/docs
|
||||
/.idea
|
||||
/server/plugins/token-refresh-scheduler.ts
|
||||
/shared/types/cim.ts
|
||||
/shared/types/feishu.ts
|
||||
104
.trae/rules/project_rules.md
Normal file
104
.trae/rules/project_rules.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Project Rules: Figma → Vue3 (with TailwindCSS4)
|
||||
|
||||
## 1. 工具链说明
|
||||
|
||||
- **IDE**: Trae IDE (>=0.5.5)
|
||||
- **MCP**: Figma AI Bridge (通过 MCP Server 与 Trae 集成)
|
||||
- **框架**: Vue3 (Composition API + `<script setup>`)
|
||||
- **样式**: TailwindCSS v4 (原子化优先,避免自定义 CSS)
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计稿 → 组件转换规则
|
||||
|
||||
### 2.1 命名与结构
|
||||
|
||||
- **组件命名**:遵循 PascalCase,例如 `ButtonPrimary.vue`。
|
||||
- **文件组织**:
|
||||
- `components/ui/` → 基础 UI 组件
|
||||
- `components/layout/` → 布局容器
|
||||
- `components/composite/` → 复合组件(由多个 UI 组件组合)
|
||||
- **Props 定义**:由 Figma 属性映射而来,必须使用 **强类型 (TypeScript)**。
|
||||
|
||||
### 2.2 TailwindCSS4 使用规范
|
||||
|
||||
- **优先使用原子类**,避免生成内联 style。
|
||||
- **颜色/间距**:映射到 Tailwind 设计令牌(如 `bg-primary`, `text-secondary`)。
|
||||
- **响应式**:使用 `sm:`, `md:`, `lg:` 前缀,避免写死像素。
|
||||
- **排版**:统一使用 `font-*` 与 `leading-*`,禁止自定义行高。
|
||||
|
||||
---
|
||||
|
||||
## 3. Figma → Vue3 布局注意事项
|
||||
|
||||
### 3.1 Flex/Grid 映射
|
||||
|
||||
- **Figma AutoLayout → Tailwind Flex**
|
||||
- 水平排列 → `flex-row`
|
||||
- 垂直排列 → `flex-col`
|
||||
- 对齐方式 → `justify-*` / `items-*`
|
||||
- **Figma Grid → Tailwind Grid**
|
||||
- 列数 → `grid-cols-*`
|
||||
- 间距 → `gap-*`
|
||||
|
||||
### 3.2 尺寸与约束
|
||||
|
||||
- **禁止固定宽高**:除非设计稿明确要求。优先使用 `w-full`, `h-auto`。
|
||||
- **最小/最大约束**:映射为 `min-w-*`, `max-h-*`,避免溢出。
|
||||
- **百分比布局**:优先转换为 `flex-grow`, `basis-*`,而非绝对定位。
|
||||
|
||||
### 3.3 绝对定位与层级
|
||||
|
||||
- **仅在必要时使用 `absolute`**(如浮层、模态框)。
|
||||
- **层级管理**:统一使用 `z-*`,避免硬编码数值。
|
||||
|
||||
---
|
||||
|
||||
## 4. AI 转换流程约束
|
||||
|
||||
1. **输入**:Figma 设计稿(需启用 Dev Mode,确保组件/Frame 命名规范)。
|
||||
2. **AI 解析**:Figma AI Bridge 提取节点 → 转换为 Vue3 组件骨架。
|
||||
3. **Tailwind 优化**:自动替换内联样式为 Tailwind 原子类。
|
||||
4. **人工校验**:开发者需检查以下内容:
|
||||
- 布局是否符合响应式规范
|
||||
- 是否存在冗余 class
|
||||
- 是否有未映射的设计 token
|
||||
|
||||
---
|
||||
|
||||
## 5. 代码示例
|
||||
|
||||
```vue
|
||||
<!-- ButtonPrimary.vue -->
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="px-4 py-2 font-medium text-white rounded-md bg-primary hover:bg-primary/80 disabled:opacity-50"
|
||||
:disabled="disabled"
|
||||
>
|
||||
{{ label }}
|
||||
</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 审查清单 (Checklist)
|
||||
|
||||
- [ ] 所有组件均为 **Vue3 `<script setup>`** 写法
|
||||
- [ ] 样式全部使用 **TailwindCSS4 原子类**
|
||||
- [ ] 布局优先使用 **Flex/Grid**,避免绝对定位
|
||||
- [ ] Props 类型定义完整,避免 `any`
|
||||
- [ ] 组件命名与 Figma Frame 保持一致
|
||||
|
||||
---
|
||||
|
||||
✅ **总结**:通过 Trae + Figma AI Bridge MCP,我们要求 **自动生成的 Vue3 组件必须保持语义化、响应式、Tailwind 原子化**,并在布局上避免固定像素与绝对定位,确保代码可维护、可扩展。
|
||||
|
||||
---
|
||||
45
.vscode/extensions.json
vendored
Normal file
45
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"recommendations": [
|
||||
// Tailwind CSS 智能感知
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"austenc.tailwind-docs",
|
||||
|
||||
// Vue 开发必备 - Vue Official (新版 Volar)
|
||||
"vue.volar",
|
||||
"vue.vscode-typescript-vue-plugin",
|
||||
|
||||
// TypeScript 支持
|
||||
"ms-vscode.vscode-typescript-next",
|
||||
|
||||
// 代码检查和格式化
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
|
||||
// CSS 相关
|
||||
"stylelint.vscode-stylelint",
|
||||
"csshoek.vscode-css-peek",
|
||||
"ecmel.vscode-html-css",
|
||||
"zignd.html-css-class-completion",
|
||||
"syler.sass-indented",
|
||||
|
||||
// 实用工具
|
||||
"formulahendry.auto-rename-tag",
|
||||
"christian-kohler.path-intellisense",
|
||||
"wix.vscode-import-cost",
|
||||
"ms-vscode.vscode-json",
|
||||
"redhat.vscode-yaml",
|
||||
"eamodio.gitlens",
|
||||
"vscode-icons-team.vscode-icons",
|
||||
"CoenraadS.bracket-pair-colorizer-2",
|
||||
"naumovs.color-highlight",
|
||||
"johnpapa.vscode-peacock",
|
||||
"gruntfuggly.todo-tree",
|
||||
"christian-kohler.npm-intellisense",
|
||||
"yzhang.markdown-all-in-one",
|
||||
"streetsidesoftware.code-spell-checker"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"vscode.typescript-language-features",
|
||||
"ms-vscode.vscode-typescript-language-features"
|
||||
]
|
||||
}
|
||||
20
.vscode/launch.json
vendored
Normal file
20
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Nuxt App",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/app/node_modules/.bin/nuxt",
|
||||
"args": ["dev"],
|
||||
"cwd": "${workspaceFolder}/app",
|
||||
"runtimeArgs": ["--inspect"],
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--inspect"
|
||||
},
|
||||
"console": "integratedTerminal",
|
||||
"restart": true,
|
||||
"protocol": "inspector"
|
||||
}
|
||||
]
|
||||
}
|
||||
227
.vscode/settings.json
vendored
Normal file
227
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,227 @@
|
||||
{
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
"**/.hg": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/Thumbs.db": true,
|
||||
"*.meta": true,
|
||||
".idea": true,
|
||||
".nuxt": true,
|
||||
"server/node_modules": true,
|
||||
"shared/node_modules": true,
|
||||
"shared/dist": true,
|
||||
"server/dist": true,
|
||||
"app/.nuxt": true,
|
||||
"app/.output": true
|
||||
},
|
||||
"explorerExclude.backup": {},
|
||||
|
||||
// 编辑器设置
|
||||
"editor.quickSuggestions": {
|
||||
"other": true,
|
||||
"comments": false,
|
||||
"strings": true
|
||||
},
|
||||
"editor.suggestOnTriggerCharacters": true,
|
||||
"editor.acceptSuggestionOnEnter": "on",
|
||||
"editor.snippetSuggestions": "inline",
|
||||
"editor.wordBasedSuggestions": "allDocuments",
|
||||
"editor.parameterHints.enabled": true,
|
||||
"editor.suggestSelection": "first",
|
||||
"editor.tabCompletion": "on",
|
||||
"editor.autoClosingBrackets": "always",
|
||||
"editor.suggest.insertMode": "replace",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
// 悬停提示设置
|
||||
"editor.hover.enabled": true,
|
||||
"editor.hover.sticky": true,
|
||||
"editor.hover.delay": 300,
|
||||
"editor.suggest.showStatusBar": true,
|
||||
"editor.suggest.preview": true,
|
||||
"editor.suggest.showKeywords": true,
|
||||
"editor.suggest.showSnippets": true,
|
||||
|
||||
// 文件关联
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss",
|
||||
"main.css": "tailwindcss"
|
||||
},
|
||||
|
||||
// Tailwind CSS IntelliSense 配置
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"vue": "html",
|
||||
"javascript": "javascript",
|
||||
"typescript": "typescript",
|
||||
"vue-html": "html",
|
||||
"plaintext": "css",
|
||||
"markdown": "html"
|
||||
},
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["class:\\s*?[\"'`]([^\"'`]*).*?[\"'`]", "[\"'`]([^\"'`]*)[\"'`]"],
|
||||
["className:\\s*?[\"'`]([^\"'`]*).*?[\"'`]", "[\"'`]([^\"'`]*)[\"'`]"],
|
||||
["\\bclass\\s*=\\s*[\"']([^\"']*)[\"']", "([^\"']*)"],
|
||||
["\\bclassName\\s*=\\s*[\"']([^\"']*)[\"']", "([^\"']*)"],
|
||||
["class=\"([^\"]*)\"", "([^\"]*)"],
|
||||
["class='([^']*)'", "([^']*)"],
|
||||
["class=`([^`]*)`", "([^`]*)"],
|
||||
["@apply\\s+([^;]*)", 1],
|
||||
["@layer utilities\\s*{[^}]*?\\.([^\\s{]+)", 1],
|
||||
["tw`([^`]*)`", 1],
|
||||
["tw=\"([^\"]*)\"", 1],
|
||||
["cva\\(([^\\)]*)\\)", 1],
|
||||
["cx\\(([^\\)]*)\\)", 1],
|
||||
["cn\\(([^\\)]*)\\)", 1]
|
||||
],
|
||||
"tailwindCSS.validate": true,
|
||||
"tailwindCSS.lint.cssConflict": "warning",
|
||||
"tailwindCSS.lint.invalidApply": "error",
|
||||
"tailwindCSS.lint.invalidConfigPath": "error",
|
||||
"tailwindCSS.lint.invalidScreen": "error",
|
||||
"tailwindCSS.lint.invalidTailwindDirective": "error",
|
||||
"tailwindCSS.lint.invalidVariant": "error",
|
||||
"tailwindCSS.lint.recommendedVariantOrder": "warning",
|
||||
"tailwindCSS.emmetCompletions": true,
|
||||
"tailwindCSS.suggestions": true,
|
||||
"tailwindCSS.hovers": true,
|
||||
"tailwindCSS.codeActions": true,
|
||||
"tailwindCSS.experimental.configFile": "./app/tailwind.config.js",
|
||||
"tailwindCSS.cssPath": "./app/assets/css/main.css",
|
||||
// Tailwind CSS 智能提示增强设置
|
||||
"tailwindCSS.experimental.classSorting": true,
|
||||
"tailwindCSS.colorDecorators": true,
|
||||
"tailwindCSS.showPixelEquivalents": true,
|
||||
"tailwindCSS.rootFontSize": 16,
|
||||
// 确保Tailwind CSS与CSS Peek兼容
|
||||
"tailwindCSS.experimental.classAttributes": ["class", "className", "ngClass"],
|
||||
|
||||
// CSS 相关设置
|
||||
"css.validate": true,
|
||||
"scss.validate": false,
|
||||
"less.validate": false,
|
||||
"css.completion.completePropertyWithSemicolon": true,
|
||||
"css.completion.triggerPropertyValueCompletion": true,
|
||||
"css.lint.unknownProperties": "warning",
|
||||
"css.lint.invalidNumber": "error",
|
||||
"css.lint.hexColorLength": "error",
|
||||
"css.lint.zeroUnits": "ignore",
|
||||
"css.lint.duplicateProperties": "warning",
|
||||
"css.lint.ieHack": "warning",
|
||||
"css.lint.argumentsInColorFunction": "error",
|
||||
"css.lint.unknownVendorSpecificProperties": "ignore",
|
||||
"css.lint.propertyIgnoredDueToDisplay": "warning",
|
||||
"css.lint.important": "warning",
|
||||
"css.lint.float": "warning",
|
||||
"css.lint.idSelector": "warning",
|
||||
"css.lint.universalSelector": "warning",
|
||||
"css.lint.boxModel": "ignore",
|
||||
"css.lint.vendorPrefix": "warning",
|
||||
"css.lint.compatibleVendorPrefixes": "warning",
|
||||
"css.lint.importStatement": "warning",
|
||||
"css.lint.fontFaceProperties": "warning",
|
||||
"css.lint.keyframeDeclarations": "warning",
|
||||
"css.lint.customPropertyWarnings": "warning",
|
||||
"css.lint.emptyRules": "warning",
|
||||
"css.lint.selectors": "warning",
|
||||
"css.lint.nesting": "warning",
|
||||
"css.lint.validProperties": [
|
||||
"composes",
|
||||
"container",
|
||||
"container-type",
|
||||
"container-name",
|
||||
"@apply",
|
||||
"@screen",
|
||||
"@layer",
|
||||
"@variants",
|
||||
"@responsive",
|
||||
"@tailwind"
|
||||
],
|
||||
|
||||
// CSS Peek 设置 - 启用CSS类悬停预览
|
||||
"cssPeek.enable": true,
|
||||
"cssPeek.enableGotoDefinition": true,
|
||||
"cssPeek.enableHover": true,
|
||||
"cssPeek.enableShowReferences": true,
|
||||
"cssPeek.enableShowReferencesInPeek": true,
|
||||
"cssPeek.enableShowDefinitionInPeek": true,
|
||||
"cssPeek.enableShowQuickInfo": true,
|
||||
"cssPeek.enableColors": true,
|
||||
"cssPeek.enableFiles": ["css", "scss", "less", "vue", "html"],
|
||||
|
||||
// HTML/CSS 类名智能提示
|
||||
"html-css-class-completion.includeGlobPattern": "**/*.{css,scss,vue,html}",
|
||||
"html-css-class-completion.enableEmmetCompletion": true,
|
||||
"html-css-class-completion.includeLanguages": {
|
||||
"vue": "html",
|
||||
"vue-html": "html"
|
||||
},
|
||||
|
||||
// Vue 相关设置
|
||||
"vetur.validation.style": false,
|
||||
"vetur.validation.template": false,
|
||||
|
||||
// Vue Official (新版 Volar) 配置
|
||||
"vue.server.hybridMode": true,
|
||||
"vue.inlayHints.missingProps": true,
|
||||
"vue.inlayHints.inlineHandlerLeading": true,
|
||||
"vue.inlayHints.optionsWrapper": true,
|
||||
"vue.inlayHints.vBindShorthand": true,
|
||||
"vue.complete.casing.tags": "kebab",
|
||||
"vue.complete.casing.props": "camel",
|
||||
|
||||
// Vue Official TypeScript 支持
|
||||
"vue.codeActions.enabled": true,
|
||||
"vue.complete.casing.tags": "kebab",
|
||||
"vue.complete.casing.props": "camel",
|
||||
"vue.suggest.enabled": true,
|
||||
"vue.suggest.includeSnippets": true,
|
||||
"vue.suggest.autoImportComponent": true,
|
||||
"vue.suggest.autoImportComponentNameCasing": "pascal",
|
||||
"vue.suggest.htmlDataSource": "html5",
|
||||
"vue.trace.server": "off",
|
||||
"vue.format.enabled": true,
|
||||
"vue.format.defaultFormatter.html": "none",
|
||||
"vue.format.defaultFormatter.css": "none",
|
||||
"vue.format.defaultFormatter.js": "none",
|
||||
"vue.format.defaultFormatter.ts": "none",
|
||||
|
||||
// Emmet 设置
|
||||
"emmet.includeLanguages": {
|
||||
"vue-html": "html",
|
||||
"vue": "html",
|
||||
"javascript": "javascriptreact",
|
||||
"typescript": "typescriptreact"
|
||||
},
|
||||
"emmet.triggerExpansionOnTab": true,
|
||||
"emmet.showAbbreviationSuggestions": true,
|
||||
"emmet.showExpandedAbbreviation": "always",
|
||||
|
||||
// TypeScript 设置
|
||||
"typescript.preferences.importModuleSpecifier": "relative",
|
||||
"typescript.suggest.autoImports": true,
|
||||
"typescript.suggest.completeFunctionCalls": true,
|
||||
"typescript.inlayHints.parameterNames.enabled": "all",
|
||||
"typescript.inlayHints.parameterTypes.enabled": true,
|
||||
"typescript.inlayHints.variableTypes.enabled": true,
|
||||
"typescript.preferences.includePackageJsonAutoImports": "on",
|
||||
"typescript.suggest.includeCompletionsWithSnippetText": true,
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
"typescript.preferences.includePackageJsonAutoImports": "on",
|
||||
"typescript.suggest.includeCompletionsWithSnippetText": true,
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
|
||||
// Nuxt 设置
|
||||
"nuxt.isNuxtApp": false,
|
||||
|
||||
// 默认使用Chrome打开链接
|
||||
"terminal.external.windowsExec": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
||||
"terminal.explorerKind": "external",
|
||||
"open-in-browser.default": "chrome",
|
||||
"svn.delete.ignoredRulesForDeletedFiles": [
|
||||
"server\\api\\CIMapi\\feishu-shanghai-cim-service.ts"
|
||||
]
|
||||
}
|
||||
119
.vscode/tailwind-workspace.code-workspace
vendored
Normal file
119
.vscode/tailwind-workspace.code-workspace
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"name": "Nuxt4Curd",
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["class:\\s*?[\"'`]([^\"'`]*).*?[\"'`]", "[\"'`]([^\"'`]*)[\"'`]"],
|
||||
["className:\\s*?[\"'`]([^\"'`]*).*?[\"'`]", "[\"'`]([^\"'`]*)[\"'`]"],
|
||||
["\\bclass\\s*=\\s*[\"']([^\"']*)[\"']", "([^\"']*)"],
|
||||
["\\bclassName\\s*=\\s*[\"']([^\"']*)[\"']", "([^\"']*)"],
|
||||
["class=\"([^\"]*)\"", "([^\"]*)"],
|
||||
["class='([^']*)'", "([^']*)"],
|
||||
["class=`([^`]*)`", "([^`]*)"],
|
||||
["@apply\\s+([^;]*)", 1],
|
||||
["@layer utilities\\s*{[^}]*?\\.([^\\s{]+)", 1],
|
||||
["tw`([^`]*)`", 1],
|
||||
["tw=\"([^\"]*)\"", 1],
|
||||
["cva\\(([^\\)]*)\\)", 1],
|
||||
["cx\\(([^\\)]*)\\)", 1],
|
||||
["cn\\(([^\\)]*)\\)", 1]
|
||||
],
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"vue": "html",
|
||||
"javascript": "javascript",
|
||||
"typescript": "typescript",
|
||||
"vue-html": "html",
|
||||
"plaintext": "css",
|
||||
"markdown": "html"
|
||||
},
|
||||
"tailwindCSS.experimental.configFile": "./app/tailwind.config.js",
|
||||
"tailwindCSS.cssPath": "./app/assets/css/main.css",
|
||||
"tailwindCSS.validate": true,
|
||||
"tailwindCSS.suggestions": true,
|
||||
"tailwindCSS.hovers": true,
|
||||
"tailwindCSS.codeActions": true,
|
||||
// Tailwind CSS 智能提示增强设置
|
||||
"tailwindCSS.experimental.classSorting": true,
|
||||
"tailwindCSS.colorDecorators": true,
|
||||
"tailwindCSS.showPixelEquivalents": true,
|
||||
"tailwindCSS.rootFontSize": 16,
|
||||
// 悬停提示设置
|
||||
"editor.hover.enabled": true,
|
||||
"editor.hover.sticky": true,
|
||||
"editor.hover.delay": 300,
|
||||
"editor.suggest.showStatusBar": true,
|
||||
"editor.suggest.preview": true,
|
||||
"editor.suggest.showKeywords": true,
|
||||
"editor.suggest.showSnippets": true,
|
||||
"css.validate": true,
|
||||
"css.completion.completePropertyWithSemicolon": true,
|
||||
"css.completion.triggerPropertyValueCompletion": true,
|
||||
// CSS Peek 设置 - 启用CSS类悬停预览
|
||||
"cssPeek.enable": true,
|
||||
"cssPeek.enableGotoDefinition": true,
|
||||
"cssPeek.enableHover": true,
|
||||
"cssPeek.enableShowReferences": true,
|
||||
"cssPeek.enableShowReferencesInPeek": true,
|
||||
"cssPeek.enableShowDefinitionInPeek": true,
|
||||
"cssPeek.enableShowQuickInfo": true,
|
||||
"cssPeek.enableColors": true,
|
||||
"cssPeek.enableFiles": ["css", "scss", "less", "vue", "html"],
|
||||
"emmet.includeLanguages": {
|
||||
"vue-html": "html",
|
||||
"vue": "html",
|
||||
"javascript": "javascriptreact",
|
||||
"typescript": "typescriptreact"
|
||||
},
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss",
|
||||
"main.css": "tailwindcss"
|
||||
},
|
||||
"typescript.preferences.importModuleSpecifier": "relative",
|
||||
"typescript.suggest.autoImports": true,
|
||||
"typescript.suggest.completeFunctionCalls": true,
|
||||
"typescript.inlayHints.parameterNames.enabled": "all",
|
||||
"typescript.inlayHints.parameterTypes.enabled": true,
|
||||
"typescript.inlayHints.variableTypes.enabled": true,
|
||||
"nuxt.isNuxtApp": true
|
||||
},
|
||||
"extensions": {
|
||||
"recommendations": [
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"austenc.tailwind-docs",
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"ms-vscode.vscode-typescript-next",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"stylelint.vscode-stylelint",
|
||||
"csshoek.vscode-css-peek",
|
||||
"ecmel.vscode-html-css",
|
||||
"zignd.html-css-class-completion",
|
||||
"syler.sass-indented",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"christian-kohler.path-intellisense",
|
||||
"wix.vscode-import-cost",
|
||||
"ms-vscode.vscode-json",
|
||||
"redhat.vscode-yaml",
|
||||
"eamodio.gitlens",
|
||||
"vscode-icons-team.vscode-icons",
|
||||
"CoenraadS.bracket-pair-colorizer-2",
|
||||
"naumovs.color-highlight",
|
||||
"johnpapa.vscode-peacock",
|
||||
"gruntfuggly.todo-tree",
|
||||
"christian-kohler.npm-intellisense",
|
||||
"yzhang.markdown-all-in-one",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"phoenisx.cssvar",
|
||||
"nuxt-framework.nuxt-ide-support"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"vscode.typescript-language-features",
|
||||
"ms-vscode.vscode-typescript-language-features"
|
||||
]
|
||||
}
|
||||
}
|
||||
12
prettier-config/index.js
Normal file
12
prettier-config/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
tabWidth: 2,
|
||||
trailingComma: 'es5',
|
||||
printWidth: 100,
|
||||
arrowParens: 'avoid',
|
||||
endOfLine: 'lf',
|
||||
bracketSpacing: true,
|
||||
jsxBracketSameLine: false,
|
||||
proseWrap: 'preserve',
|
||||
}
|
||||
16
prettier-config/package.json
Normal file
16
prettier-config/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@nuxt4crud/prettier-config",
|
||||
"version": "1.0.0",
|
||||
"description": "Prettier configuration for Nuxt4CRUD project",
|
||||
"main": "index.js",
|
||||
"files": [
|
||||
"index.js"
|
||||
],
|
||||
"keywords": [
|
||||
"prettier",
|
||||
"config",
|
||||
"nuxt4",
|
||||
"crud"
|
||||
],
|
||||
"license": "MIT"
|
||||
}
|
||||
1
server/.env
Normal file
1
server/.env
Normal file
@@ -0,0 +1 @@
|
||||
DATABASE_URL=file:./dev.db
|
||||
3
server/.npmrc
Normal file
3
server/.npmrc
Normal file
@@ -0,0 +1,3 @@
|
||||
legacy-peer-deps=true
|
||||
fund=false
|
||||
audit=false
|
||||
46
server/api/[table]/[id].delete.ts
Normal file
46
server/api/[table]/[id].delete.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { defineEventHandler, getRouterParam, createError } from 'h3'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { BaseCrudService } from '../../lib/crud-service'
|
||||
import { createCrudHandlers } from '../../lib/crud-handler'
|
||||
import { Prisma } from '@prisma/client'
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const table = getRouterParam(event, 'table')?.toLowerCase()
|
||||
const id = getRouterParam(event, 'id')
|
||||
|
||||
if (!table) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Table name is required',
|
||||
})
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Record ID is required',
|
||||
})
|
||||
}
|
||||
|
||||
// 将表名转换为 PascalCase 模型名(首字母大写,复数转单数)
|
||||
let modelName = table
|
||||
// 简单的复数转单数逻辑(去掉结尾的s)
|
||||
if (modelName.endsWith('s')) {
|
||||
modelName = modelName.slice(0, -1)
|
||||
}
|
||||
modelName = modelName.charAt(0).toUpperCase() + modelName.slice(1)
|
||||
|
||||
// 验证表名是否存在于 Prisma 模型中
|
||||
if (!prisma[modelName as keyof typeof prisma]) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Table not found or not supported',
|
||||
})
|
||||
}
|
||||
|
||||
// 直接实例化 BaseCrudService,传入动态表名
|
||||
const service = new BaseCrudService(prisma, modelName as Prisma.ModelName)
|
||||
|
||||
const { delete: remove } = createCrudHandlers(service)
|
||||
return await remove(event)
|
||||
})
|
||||
46
server/api/[table]/[id].get.ts
Normal file
46
server/api/[table]/[id].get.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { defineEventHandler, getRouterParam, createError } from 'h3'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { BaseCrudService } from '../../lib/crud-service'
|
||||
import { createCrudHandlers } from '../../lib/crud-handler'
|
||||
import { Prisma } from '@prisma/client'
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const table = getRouterParam(event, 'table')?.toLowerCase()
|
||||
const id = getRouterParam(event, 'id')
|
||||
|
||||
if (!table) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Table name is required',
|
||||
})
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Record ID is required',
|
||||
})
|
||||
}
|
||||
|
||||
// 将表名转换为 PascalCase 模型名(首字母大写,复数转单数)
|
||||
let modelName = table
|
||||
// 简单的复数转单数逻辑(去掉结尾的s)
|
||||
if (modelName.endsWith('s')) {
|
||||
modelName = modelName.slice(0, -1)
|
||||
}
|
||||
modelName = modelName.charAt(0).toUpperCase() + modelName.slice(1)
|
||||
|
||||
// 验证表名是否存在于 Prisma 模型中
|
||||
if (!prisma[modelName as keyof typeof prisma]) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Table not found or not supported',
|
||||
})
|
||||
}
|
||||
|
||||
// 直接实例化 BaseCrudService,传入动态表名
|
||||
const service = new BaseCrudService(prisma, modelName as Prisma.ModelName)
|
||||
|
||||
const { get } = createCrudHandlers(service)
|
||||
return await get(event)
|
||||
})
|
||||
59
server/api/[table]/[id].put.ts
Normal file
59
server/api/[table]/[id].put.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { defineEventHandler, getRouterParam, createError } from 'h3'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { BaseCrudService } from '../../lib/crud-service'
|
||||
import { createCrudHandlers } from '../../lib/crud-handler'
|
||||
import { Prisma } from '@prisma/client'
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const table = getRouterParam(event, 'table')?.toLowerCase()
|
||||
const id = getRouterParam(event, 'id')
|
||||
|
||||
if (!table) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Table name is required',
|
||||
})
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Record ID is required',
|
||||
})
|
||||
}
|
||||
|
||||
// 将表名转换为 PascalCase 模型名(首字母大写,复数转单数)
|
||||
let modelName = table
|
||||
// 简单的复数转单数逻辑(去掉结尾的s)
|
||||
if (modelName.endsWith('s')) {
|
||||
modelName = modelName.slice(0, -1)
|
||||
}
|
||||
modelName = modelName.charAt(0).toUpperCase() + modelName.slice(1)
|
||||
|
||||
// 验证表名是否存在于 Prisma 模型中
|
||||
if (!prisma[modelName as keyof typeof prisma]) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Table not found or not supported',
|
||||
})
|
||||
}
|
||||
|
||||
// 直接实例化 BaseCrudService,传入动态表名
|
||||
const service = new BaseCrudService(prisma, modelName as Prisma.ModelName)
|
||||
|
||||
// 如果是 posts 表,添加标题验证
|
||||
if (table === 'posts') {
|
||||
const originalValidateUpdate = service['validateUpdate']
|
||||
service['validateUpdate'] = async (id: number, data: any, existing: any) => {
|
||||
if (!data.title) {
|
||||
const error = new Error('Title is required')
|
||||
;(error as any).statusCode = 400
|
||||
throw error
|
||||
}
|
||||
await originalValidateUpdate.call(service, id, data, existing)
|
||||
}
|
||||
}
|
||||
|
||||
const { update } = createCrudHandlers(service)
|
||||
return await update(event)
|
||||
})
|
||||
42
server/api/[table]/index.get.ts
Normal file
42
server/api/[table]/index.get.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { defineEventHandler, getQuery, getRouterParam, createError } from 'h3'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { BaseCrudService } from '../../lib/crud-service'
|
||||
import { createCrudHandlers } from '../../lib/crud-handler'
|
||||
import { Prisma } from '@prisma/client'
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const table = getRouterParam(event, 'table')?.toLowerCase()
|
||||
|
||||
if (!table) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Table name is required',
|
||||
})
|
||||
}
|
||||
|
||||
// 将表名转换为 PascalCase 模型名(首字母大写,复数转单数)
|
||||
let modelName = table
|
||||
// 简单的复数转单数逻辑(去掉结尾的s)
|
||||
if (modelName.endsWith('s')) {
|
||||
modelName = modelName.slice(0, -1)
|
||||
}
|
||||
modelName = modelName.charAt(0).toUpperCase() + modelName.slice(1)
|
||||
|
||||
// 验证表名是否存在于 Prisma 模型中
|
||||
if (!prisma[modelName as keyof typeof prisma]) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Table not found or not supported',
|
||||
})
|
||||
}
|
||||
|
||||
// 直接实例化 BaseCrudService,传入动态表名
|
||||
const service = new BaseCrudService(prisma, modelName as Prisma.ModelName, {
|
||||
searchableFields: [],
|
||||
defaultOrderBy: 'createdAt',
|
||||
defaultOrderDirection: 'desc',
|
||||
})
|
||||
|
||||
const { list } = createCrudHandlers(service)
|
||||
return await list(event)
|
||||
})
|
||||
51
server/api/[table]/index.post.ts
Normal file
51
server/api/[table]/index.post.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { defineEventHandler, getRouterParam, createError } from 'h3'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { BaseCrudService } from '../../lib/crud-service'
|
||||
import { createCrudHandlers } from '../../lib/crud-handler'
|
||||
import { Prisma } from '@prisma/client'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const table = getRouterParam(event, 'table')?.toLowerCase()
|
||||
|
||||
if (!table) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Table name is required',
|
||||
})
|
||||
}
|
||||
|
||||
// 将表名转换为 PascalCase 模型名(首字母大写,复数转单数)
|
||||
let modelName = table
|
||||
// 简单的复数转单数逻辑(去掉结尾的s)
|
||||
if (modelName.endsWith('s')) {
|
||||
modelName = modelName.slice(0, -1)
|
||||
}
|
||||
modelName = modelName.charAt(0).toUpperCase() + modelName.slice(1)
|
||||
|
||||
// 验证表名是否存在于 Prisma 模型中
|
||||
if (!prisma[modelName as keyof typeof prisma]) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Table not found or not supported',
|
||||
})
|
||||
}
|
||||
|
||||
// 直接实例化 BaseCrudService,传入动态表名
|
||||
const service = new BaseCrudService(prisma, modelName as Prisma.ModelName)
|
||||
|
||||
// 如果是 posts 表,添加标题验证
|
||||
if (table === 'posts') {
|
||||
const originalValidateCreate = service['validateCreate']
|
||||
service['validateCreate'] = async (data: any) => {
|
||||
if (!data.title) {
|
||||
const error = new Error('Title is required')
|
||||
;(error as any).statusCode = 400
|
||||
throw error
|
||||
}
|
||||
await originalValidateCreate.call(service, data)
|
||||
}
|
||||
}
|
||||
|
||||
const { create } = createCrudHandlers(service)
|
||||
return await create(event)
|
||||
})
|
||||
150
server/api/table-info.get.ts
Normal file
150
server/api/table-info.get.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 表信息API接口
|
||||
* Table Information API Endpoint
|
||||
*/
|
||||
import { createSqlQueryBuilder } from '../lib/sql-query-builder'
|
||||
import { prisma } from '../lib/prisma'
|
||||
|
||||
/**
|
||||
* 支持的表名映射
|
||||
* Supported table names mapping
|
||||
*/
|
||||
const ALLOWED_TABLES = {
|
||||
Post: 'posts' // 支持Post表
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 字段类型映射
|
||||
* Field type mapping
|
||||
*/
|
||||
const FIELD_TYPES: Record<string, Record<string, string>> = {
|
||||
posts: {
|
||||
id: 'number',
|
||||
title: 'string',
|
||||
content: 'string',
|
||||
published: 'boolean',
|
||||
createdAt: 'datetime',
|
||||
updatedAt: 'datetime',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认搜索字段配置
|
||||
* Default search fields configuration
|
||||
*/
|
||||
const DEFAULT_SEARCH_FIELDS: Record<string, string[]> = {
|
||||
posts: ['title', 'content'], // posts表搜索字段
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认排序字段配置
|
||||
* Default sortable fields configuration
|
||||
*/
|
||||
const DEFAULT_SORTABLE_FIELDS: Record<string, string[]> = {
|
||||
posts: ['id', 'title', 'content', 'published', 'createdAt', 'updatedAt'], // posts表排序字段
|
||||
}
|
||||
|
||||
/**
|
||||
* 表信息API处理器
|
||||
* Table information API handler
|
||||
*/
|
||||
export default defineEventHandler(async event => {
|
||||
try {
|
||||
// 获取查询参数
|
||||
const query = getQuery(event)
|
||||
const tableName = query.table as string
|
||||
|
||||
// 如果没有指定表名,返回所有支持的表
|
||||
if (!tableName) {
|
||||
const supportedTables = Object.keys(ALLOWED_TABLES).map(key => ({
|
||||
key,
|
||||
name: ALLOWED_TABLES[key as keyof typeof ALLOWED_TABLES],
|
||||
searchFields:
|
||||
DEFAULT_SEARCH_FIELDS[ALLOWED_TABLES[key as keyof typeof ALLOWED_TABLES]] || [],
|
||||
sortableFields:
|
||||
DEFAULT_SORTABLE_FIELDS[ALLOWED_TABLES[key as keyof typeof ALLOWED_TABLES]] || [],
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
tables: supportedTables,
|
||||
},
|
||||
message: '获取支持的表列表成功',
|
||||
}
|
||||
}
|
||||
|
||||
// 验证表名是否允许
|
||||
const actualTableName = ALLOWED_TABLES[tableName as keyof typeof ALLOWED_TABLES]
|
||||
if (!actualTableName) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `不支持的表: ${tableName}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 创建SQL查询构造器
|
||||
const queryBuilder = createSqlQueryBuilder(prisma)
|
||||
|
||||
// 获取表字段信息
|
||||
const fields = await queryBuilder.getTableFields(actualTableName)
|
||||
|
||||
// 构建字段详细信息
|
||||
const fieldDetails = fields.map(field => ({
|
||||
name: field,
|
||||
type: FIELD_TYPES[actualTableName]?.[field] || 'string',
|
||||
searchable: DEFAULT_SEARCH_FIELDS[actualTableName]?.includes(field) || false,
|
||||
sortable: DEFAULT_SORTABLE_FIELDS[actualTableName]?.includes(field) || true,
|
||||
}))
|
||||
|
||||
// 获取支持的操作符
|
||||
const operators = [
|
||||
{ value: 'equals', label: '等于', types: ['string', 'number', 'boolean'] },
|
||||
{ value: 'not_equals', label: '不等于', types: ['string', 'number', 'boolean'] },
|
||||
{ value: 'greater_than', label: '大于', types: ['number', 'datetime'] },
|
||||
{ value: 'greater_than_or_equal', label: '大于等于', types: ['number', 'datetime'] },
|
||||
{ value: 'less_than', label: '小于', types: ['number', 'datetime'] },
|
||||
{ value: 'less_than_or_equal', label: '小于等于', types: ['number', 'datetime'] },
|
||||
{ value: 'contains', label: '包含', types: ['string'] },
|
||||
{ value: 'starts_with', label: '开始于', types: ['string'] },
|
||||
{ value: 'ends_with', label: '结束于', types: ['string'] },
|
||||
{ value: 'in', label: '在列表中', types: ['string', 'number'] },
|
||||
{ value: 'not_in', label: '不在列表中', types: ['string', 'number'] },
|
||||
{ value: 'is_null', label: '为空', types: ['string', 'number', 'datetime'] },
|
||||
{ value: 'is_not_null', label: '不为空', types: ['string', 'number', 'datetime'] },
|
||||
{ value: 'between', label: '在范围内', types: ['number', 'datetime'] },
|
||||
{ value: 'date_equals', label: '日期等于', types: ['datetime'] },
|
||||
{ value: 'date_before', label: '日期之前', types: ['datetime'] },
|
||||
{ value: 'date_after', label: '日期之后', types: ['datetime'] },
|
||||
{ value: 'date_between', label: '日期范围', types: ['datetime'] },
|
||||
]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
table: {
|
||||
name: actualTableName,
|
||||
key: tableName,
|
||||
fields: fieldDetails,
|
||||
searchFields: DEFAULT_SEARCH_FIELDS[actualTableName] || [],
|
||||
sortableFields: DEFAULT_SORTABLE_FIELDS[actualTableName] || fields,
|
||||
operators,
|
||||
},
|
||||
},
|
||||
message: '获取表信息成功',
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Table info API error:', error)
|
||||
|
||||
// 如果是已知错误,直接抛出
|
||||
if (error && typeof error === 'object' && 'statusCode' in error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// 处理未知错误
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: `获取表信息失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
})
|
||||
}
|
||||
})
|
||||
136
server/api/universal-query.post.ts
Normal file
136
server/api/universal-query.post.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 通用高级查询API接口
|
||||
* Universal Advanced Query API Endpoint
|
||||
*/
|
||||
import type { AdvancedQueryOptions } from '@nuxt4crud/shared'
|
||||
import { prisma } from '../lib/prisma'
|
||||
import { createSqlQueryBuilder } from '../lib/sql-query-builder'
|
||||
|
||||
/**
|
||||
* 通用查询请求体接口
|
||||
* Universal query request body interface
|
||||
*/
|
||||
interface UniversalQueryRequest {
|
||||
table: string
|
||||
options: AdvancedQueryOptions
|
||||
searchFields?: string[]
|
||||
mode?: 'query' | 'aggregate'
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持的表名映射(安全检查)
|
||||
* Supported table names mapping (security check)
|
||||
*/
|
||||
const ALLOWED_TABLES = {
|
||||
Post: 'posts', // 支持 Post 表
|
||||
User: 'users', // 支持 User 表
|
||||
// 可以在这里添加更多允许查询的表
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 默认搜索字段配置
|
||||
* Default search fields configuration
|
||||
*/
|
||||
const DEFAULT_SEARCH_FIELDS: Record<string, string[]> = {
|
||||
posts: ['title', 'content'], // Post 表搜索字段
|
||||
users: ['name', 'email'], // User 表搜索字段
|
||||
// 可以为其他表配置默认搜索字段
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用高级查询API处理器
|
||||
* Universal advanced query API handler
|
||||
*/
|
||||
export default defineEventHandler(async event => {
|
||||
try {
|
||||
// 只允许POST方法
|
||||
assertMethod(event, 'POST')
|
||||
|
||||
// 获取请求体
|
||||
const body = (await readBody(event)) as UniversalQueryRequest
|
||||
|
||||
// 验证请求参数
|
||||
if (!body.table || !body.options) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: '缺少必要参数: table 和 options',
|
||||
})
|
||||
}
|
||||
|
||||
// 安全检查:验证表名是否允许
|
||||
const tableName = ALLOWED_TABLES[body.table as keyof typeof ALLOWED_TABLES]
|
||||
if (!tableName) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `不允许查询表: ${body.table}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 创建SQL查询构造器
|
||||
const queryBuilder = createSqlQueryBuilder(prisma)
|
||||
|
||||
// 获取表字段信息
|
||||
const availableFields = await queryBuilder.getTableFields(tableName)
|
||||
|
||||
// 验证查询选项
|
||||
const validationErrors = queryBuilder.validateQueryOptions(body.options, availableFields)
|
||||
if (validationErrors.length > 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `查询参数验证失败: ${validationErrors.join(', ')}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取搜索字段
|
||||
const searchFields = body.searchFields || DEFAULT_SEARCH_FIELDS[tableName] || []
|
||||
|
||||
// 执行查询
|
||||
let result: any
|
||||
|
||||
if (body.mode === 'aggregate') {
|
||||
// 执行聚合查询
|
||||
result = await queryBuilder.executeAggregateQuery(tableName, body.options)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
table: tableName,
|
||||
mode: 'aggregate',
|
||||
result,
|
||||
options: body.options,
|
||||
availableFields,
|
||||
searchFields,
|
||||
},
|
||||
message: '聚合查询执行成功',
|
||||
}
|
||||
} else {
|
||||
// 执行普通查询
|
||||
result = await queryBuilder.executeQuery(tableName, body.options, searchFields)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
table: tableName,
|
||||
mode: 'query',
|
||||
...result,
|
||||
availableFields,
|
||||
searchFields,
|
||||
},
|
||||
message: '查询执行成功',
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Universal query API error:', error)
|
||||
|
||||
// 如果是已知错误,直接抛出
|
||||
if (error && typeof error === 'object' && 'statusCode' in error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// 处理未知错误
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: `查询执行失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
})
|
||||
}
|
||||
})
|
||||
28
server/config/feishu-config.ts
Normal file
28
server/config/feishu-config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 飞书应用配置
|
||||
* 将敏感信息配置在服务端,避免前端暴露
|
||||
*/
|
||||
|
||||
export const FEISHU_CONFIG = {
|
||||
// 默认应用配置 - 从环境变量获取
|
||||
DEFAULT_APP_TOKEN: process.env.FEISHU_DEFAULT_APP_TOKEN || 'YDtMbwqBkangyistqmicDAH0nag',
|
||||
DEFAULT_TABLE_ID: process.env.FEISHU_DEFAULT_TABLE_ID || 'tblGRc1MSmQfonb2',
|
||||
|
||||
// 应用凭证 - 从环境变量获取
|
||||
APP_ID: process.env.FEISHU_APP_ID || '',
|
||||
APP_SECRET: process.env.FEISHU_APP_SECRET || '',
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取飞书应用配置
|
||||
* @param tableId 可选的表格ID,如果不提供则使用默认配置
|
||||
* @returns 返回完整的配置信息(app_token完全从服务端获取)
|
||||
*/
|
||||
export function getFeishuConfig(tableId?: string) {
|
||||
return {
|
||||
appToken: FEISHU_CONFIG.DEFAULT_APP_TOKEN, // app_token完全从服务端配置获取,不从前端传递
|
||||
tableId: tableId || FEISHU_CONFIG.DEFAULT_TABLE_ID,
|
||||
appId: FEISHU_CONFIG.APP_ID,
|
||||
appSecret: FEISHU_CONFIG.APP_SECRET,
|
||||
}
|
||||
}
|
||||
337
server/lib/crud-handler.ts
Normal file
337
server/lib/crud-handler.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* 通用 CRUD 处理器基类
|
||||
* Generic CRUD Handler Base Class
|
||||
*
|
||||
* 提供标准的 HTTP 请求处理和响应格式化
|
||||
* Provides standard HTTP request handling and response formatting
|
||||
*/
|
||||
|
||||
import type { H3Event } from 'h3'
|
||||
import { getQuery, getRouterParams, readBody, createError } from 'h3'
|
||||
import { validateQuery, validateBody, validateParams } from '@nuxt4crud/shared'
|
||||
import type {
|
||||
BaseEntity,
|
||||
CreateInput,
|
||||
UpdateInput,
|
||||
QueryParams,
|
||||
ApiResponse,
|
||||
PaginatedResponse,
|
||||
} from '@nuxt4crud/shared'
|
||||
|
||||
/**
|
||||
* CRUD 处理器基类
|
||||
* CRUD Handler Base Class
|
||||
*/
|
||||
export abstract class BaseCrudHandler<
|
||||
T extends BaseEntity,
|
||||
CreateInputType extends CreateInput,
|
||||
UpdateInputType extends UpdateInput,
|
||||
QueryParamsType extends QueryParams = QueryParams,
|
||||
> {
|
||||
constructor(
|
||||
protected service: any, // 实际的服务实例
|
||||
protected schemas: {
|
||||
query?: any
|
||||
create?: any
|
||||
update?: any
|
||||
params?: any
|
||||
} = {}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 处理列表查询请求
|
||||
* Handle list query request
|
||||
*/
|
||||
async handleList(event: H3Event): Promise<ApiResponse<PaginatedResponse<T>>> {
|
||||
try {
|
||||
const query = this.schemas.query
|
||||
? validateQuery(this.schemas.query, getQuery(event))
|
||||
: getQuery(event)
|
||||
|
||||
const result = await this.service.findMany(query)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
}
|
||||
} catch (error: any) {
|
||||
return this.handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个记录查询请求
|
||||
* Handle single record query request
|
||||
*/
|
||||
async handleGet(event: H3Event): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
const params = this.schemas.params
|
||||
? validateParams(this.schemas.params, getRouterParams(event))
|
||||
: getRouterParams(event)
|
||||
|
||||
const id = Number(params.id)
|
||||
const result = await this.service.findById(id)
|
||||
|
||||
if (!result) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: '记录不存在',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
}
|
||||
} catch (error: any) {
|
||||
return this.handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容单个或多个ID的查询请求(支持 GET 与 POST)
|
||||
* Flexible handler to fetch by a single ID or multiple IDs (supports GET and POST)
|
||||
*
|
||||
* - GET: 路径参数 `id`,支持逗号分隔(如 `/api/.../1,2,3`)
|
||||
* - POST: 请求体包含 `id: number` 或 `ids: number[] | string`
|
||||
*/
|
||||
async handleGetFlexible(event: H3Event): Promise<ApiResponse<T | T[]>> {
|
||||
try {
|
||||
const method = event?.node?.req?.method || 'GET'
|
||||
|
||||
// POST 支持通过请求体传输多个ID
|
||||
if (method === 'POST') {
|
||||
const body = await readBody(event)
|
||||
|
||||
if (!body || (body.id === undefined && body.ids === undefined)) {
|
||||
throw createError({ statusCode: 400, statusMessage: '请求体必须包含 id 或 ids' })
|
||||
}
|
||||
|
||||
// 归一化为ID数组
|
||||
const ids: number[] = Array.isArray(body.ids)
|
||||
? body.ids.map((v: any) => Number(v)).filter((v: number) => Number.isFinite(v) && v > 0)
|
||||
: typeof body.ids === 'string'
|
||||
? body.ids
|
||||
.split(',')
|
||||
.map(s => Number(s.trim()))
|
||||
.filter(n => Number.isFinite(n) && n > 0)
|
||||
: body.id !== undefined
|
||||
? [Number(body.id)].filter(n => Number.isFinite(n) && n > 0)
|
||||
: []
|
||||
|
||||
if (ids.length === 0) {
|
||||
throw createError({ statusCode: 400, statusMessage: '无效的ID或ID数组' })
|
||||
}
|
||||
|
||||
// 单个ID与多个ID统一处理
|
||||
if (ids.length === 1) {
|
||||
const one = await this.service.findById(ids[0])
|
||||
if (!one) {
|
||||
throw createError({ statusCode: 404, statusMessage: '记录不存在' })
|
||||
}
|
||||
return { success: true, data: one }
|
||||
} else {
|
||||
const list = await this.service.findByIds(ids)
|
||||
if (!list || list.length === 0) {
|
||||
throw createError({ statusCode: 404, statusMessage: '记录不存在' })
|
||||
}
|
||||
const partial = list.length < ids.length
|
||||
return {
|
||||
success: true,
|
||||
data: list,
|
||||
message: partial ? '部分ID未找到' : undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GET 支持 `/api/.../[id]` 单个ID,或逗号分隔的多个ID
|
||||
const params = getRouterParams(event)
|
||||
if (!params || params.id === undefined) {
|
||||
throw createError({ statusCode: 400, statusMessage: '缺少路径参数 id' })
|
||||
}
|
||||
|
||||
const rawId = String(params.id)
|
||||
const ids = rawId
|
||||
.split(',')
|
||||
.map(s => Number(s.trim()))
|
||||
.filter(n => Number.isFinite(n) && n > 0)
|
||||
|
||||
if (ids.length === 0) {
|
||||
throw createError({ statusCode: 400, statusMessage: '无效的路径参数 id' })
|
||||
}
|
||||
|
||||
if (ids.length === 1) {
|
||||
const one = await this.service.findById(ids[0])
|
||||
if (!one) {
|
||||
throw createError({ statusCode: 404, statusMessage: '记录不存在' })
|
||||
}
|
||||
return { success: true, data: one }
|
||||
} else {
|
||||
const list = await this.service.findByIds(ids)
|
||||
if (!list || list.length === 0) {
|
||||
throw createError({ statusCode: 404, statusMessage: '记录不存在' })
|
||||
}
|
||||
const partial = list.length < ids.length
|
||||
return {
|
||||
success: true,
|
||||
data: list,
|
||||
message: partial ? '部分ID未找到' : undefined,
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
return this.handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理创建请求
|
||||
* Handle create request
|
||||
*/
|
||||
async handleCreate(event: H3Event): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
const body = await readBody(event)
|
||||
|
||||
if (!body) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: '请求体不能为空',
|
||||
})
|
||||
}
|
||||
|
||||
const data = this.schemas.create ? validateBody(this.schemas.create, body) : body
|
||||
|
||||
const result = await this.service.create(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
message: '创建成功',
|
||||
}
|
||||
} catch (error: any) {
|
||||
return this.handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理更新请求
|
||||
* Handle update request
|
||||
*/
|
||||
async handleUpdate(event: H3Event): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
const params = this.schemas.params
|
||||
? validateParams(this.schemas.params, getRouterParams(event))
|
||||
: getRouterParams(event)
|
||||
|
||||
const id = Number(params.id)
|
||||
const body = await readBody(event)
|
||||
|
||||
if (!body) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: '请求体不能为空',
|
||||
})
|
||||
}
|
||||
|
||||
const data = this.schemas.update ? validateBody(this.schemas.update, body) : body
|
||||
|
||||
const result = await this.service.update(id, data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
message: '更新成功',
|
||||
}
|
||||
} catch (error: any) {
|
||||
return this.handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理删除请求
|
||||
* Handle delete request
|
||||
*/
|
||||
async handleDelete(event: H3Event): Promise<ApiResponse<void>> {
|
||||
try {
|
||||
const params = this.schemas.params
|
||||
? validateParams(this.schemas.params, getRouterParams(event))
|
||||
: getRouterParams(event)
|
||||
|
||||
const id = Number(params.id)
|
||||
await this.service.delete(id)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '删除成功',
|
||||
}
|
||||
} catch (error: any) {
|
||||
return this.handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误处理
|
||||
* Error handling
|
||||
*/
|
||||
protected handleError(error: any): ApiResponse {
|
||||
console.error('CRUD Handler Error:', error)
|
||||
|
||||
if (error.statusCode === 404) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '记录不存在',
|
||||
}
|
||||
}
|
||||
|
||||
if (error.statusCode === 400) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '请求参数错误',
|
||||
validationErrors: error.validationErrors,
|
||||
}
|
||||
}
|
||||
|
||||
if (error.statusCode === 409) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '数据冲突',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '服务器内部错误',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 CRUD 处理器工厂函数
|
||||
* Create CRUD handler factory function
|
||||
*/
|
||||
export function createCrudHandlers<
|
||||
T extends BaseEntity,
|
||||
CreateInputType extends CreateInput,
|
||||
UpdateInputType extends UpdateInput,
|
||||
QueryParamsType extends QueryParams = QueryParams,
|
||||
>(
|
||||
service: any,
|
||||
schemas: {
|
||||
query?: any
|
||||
create?: any
|
||||
update?: any
|
||||
params?: any
|
||||
} = {}
|
||||
) {
|
||||
const handler = new BaseCrudHandler<T, CreateInputType, UpdateInputType, QueryParamsType>(
|
||||
service,
|
||||
schemas
|
||||
)
|
||||
|
||||
return {
|
||||
list: (event: H3Event) => handler.handleList(event),
|
||||
get: (event: H3Event) => handler.handleGet(event),
|
||||
create: (event: H3Event) => handler.handleCreate(event),
|
||||
update: (event: H3Event) => handler.handleUpdate(event),
|
||||
delete: (event: H3Event) => handler.handleDelete(event),
|
||||
}
|
||||
}
|
||||
252
server/lib/crud-service.ts
Normal file
252
server/lib/crud-service.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* 通用 CRUD 服务基类
|
||||
* Generic CRUD Service Base Class
|
||||
*
|
||||
* 提供标准的 CRUD 操作,支持分页、搜索、排序等功能
|
||||
* Provides standard CRUD operations with pagination, search, and sorting
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import type { BaseEntity, PaginatedResponse, CrudOptions } from '@nuxt4crud/shared'
|
||||
|
||||
/**
|
||||
* CRUD 服务基类
|
||||
* CRUD Service Base Class
|
||||
*/
|
||||
export abstract class BaseCrudService<
|
||||
T extends BaseEntity,
|
||||
CreateInput,
|
||||
UpdateInput,
|
||||
QueryParams = any,
|
||||
> {
|
||||
protected model: any
|
||||
protected options: CrudOptions
|
||||
|
||||
constructor(
|
||||
protected prisma: PrismaClient,
|
||||
protected modelName: string,
|
||||
options: Partial<CrudOptions> = {}
|
||||
) {
|
||||
this.model = (prisma as any)[modelName]
|
||||
this.options = {
|
||||
softDelete: false,
|
||||
defaultOrderBy: 'createdAt',
|
||||
defaultOrderDirection: 'desc',
|
||||
searchableFields: [],
|
||||
uniqueFields: [],
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表(支持分页、搜索、排序)
|
||||
* Get list with pagination, search, and sorting
|
||||
*/
|
||||
async findMany(
|
||||
params: QueryParams & {
|
||||
page?: number
|
||||
limit?: number
|
||||
search?: string
|
||||
orderBy?: string
|
||||
orderDirection?: 'asc' | 'desc'
|
||||
}
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
search = '',
|
||||
orderBy = this.options.defaultOrderBy,
|
||||
orderDirection = this.options.defaultOrderDirection,
|
||||
...filters
|
||||
} = params
|
||||
|
||||
const where = this.buildWhereClause(search, filters)
|
||||
|
||||
const skip = (page - 1) * limit
|
||||
const take = limit
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
this.model.findMany({
|
||||
where,
|
||||
skip,
|
||||
take,
|
||||
orderBy: { [orderBy]: orderDirection },
|
||||
}),
|
||||
this.model.count({ where }),
|
||||
])
|
||||
|
||||
return {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找
|
||||
* Find by ID
|
||||
*/
|
||||
async findById(id: number): Promise<T | null> {
|
||||
return await this.model.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据多个ID查找记录
|
||||
* Find records by multiple IDs
|
||||
*
|
||||
* @param ids 要查询的ID数组
|
||||
* @returns 匹配的记录列表
|
||||
*/
|
||||
async findByIds(ids: number[]): Promise<T[]> {
|
||||
const normalizedIds = Array.from(
|
||||
new Set(
|
||||
(ids || [])
|
||||
.map(id => Number(id))
|
||||
.filter(id => Number.isFinite(id) && id > 0)
|
||||
)
|
||||
)
|
||||
|
||||
if (normalizedIds.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return await this.model.findMany({
|
||||
where: { id: { in: normalizedIds } },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建记录
|
||||
* Create record
|
||||
*/
|
||||
async create(data: CreateInput): Promise<T> {
|
||||
await this.validateCreate(data)
|
||||
|
||||
const transformedData = await this.transformCreateData(data)
|
||||
|
||||
return await this.model.create({
|
||||
data: transformedData,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新记录
|
||||
* Update record
|
||||
*/
|
||||
async update(id: number, data: UpdateInput): Promise<T> {
|
||||
const existing = await this.findById(id)
|
||||
if (!existing) {
|
||||
const error = new Error('记录不存在')
|
||||
;(error as any).statusCode = 404
|
||||
throw error
|
||||
}
|
||||
|
||||
await this.validateUpdate(id, data, existing)
|
||||
|
||||
const transformedData = await this.transformUpdateData(data, existing)
|
||||
|
||||
return await this.model.update({
|
||||
where: { id },
|
||||
data: transformedData,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除记录
|
||||
* Delete record
|
||||
*/
|
||||
async delete(id: number): Promise<void> {
|
||||
const existing = await this.findById(id)
|
||||
if (!existing) {
|
||||
const error = new Error('记录不存在')
|
||||
;(error as any).statusCode = 404
|
||||
throw error
|
||||
}
|
||||
|
||||
await this.validateDelete(id, existing)
|
||||
|
||||
if (this.options.softDelete) {
|
||||
await this.model.update({
|
||||
where: { id },
|
||||
data: { deletedAt: new Date() },
|
||||
})
|
||||
} else {
|
||||
await this.model.delete({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询条件
|
||||
* Build where clause
|
||||
*/
|
||||
protected buildWhereClause(search: string, filters: any): any {
|
||||
const where: any = {}
|
||||
|
||||
// 搜索功能
|
||||
if (search && this.options.searchableFields?.length) {
|
||||
where.OR = this.options.searchableFields.map(field => ({
|
||||
[field]: { contains: search },
|
||||
}))
|
||||
}
|
||||
|
||||
// 过滤条件
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && key !== 'search') {
|
||||
where[key] = value
|
||||
}
|
||||
})
|
||||
|
||||
// 软删除过滤
|
||||
if (this.options.softDelete) {
|
||||
where.deletedAt = null
|
||||
}
|
||||
|
||||
return where
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建前验证钩子
|
||||
* Pre-create validation hook
|
||||
*/
|
||||
protected async validateCreate(data: CreateInput): Promise<void> {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新前验证钩子
|
||||
* Pre-update validation hook
|
||||
*/
|
||||
protected async validateUpdate(id: number, data: UpdateInput, existing: T): Promise<void> {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除前验证钩子
|
||||
* Pre-delete validation hook
|
||||
*/
|
||||
protected async validateDelete(id: number, existing: T): Promise<void> {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据转换钩子
|
||||
* Create data transformation hook
|
||||
*/
|
||||
protected async transformCreateData(data: CreateInput): Promise<any> {
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据转换钩子
|
||||
* Update data transformation hook
|
||||
*/
|
||||
protected async transformUpdateData(data: UpdateInput, existing: T): Promise<any> {
|
||||
return data
|
||||
}
|
||||
}
|
||||
22
server/lib/prisma.ts
Normal file
22
server/lib/prisma.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Prisma 客户端实例
|
||||
* Prisma Client Instance
|
||||
*/
|
||||
|
||||
import { createRequire } from 'node:module'
|
||||
const require = createRequire(import.meta.url)
|
||||
const { PrismaClient } = require('@prisma/client')
|
||||
|
||||
/**
|
||||
* 全局Prisma客户端实例
|
||||
* Global Prisma Client instance
|
||||
*/
|
||||
export const prisma = new PrismaClient()
|
||||
|
||||
/**
|
||||
* 在应用关闭时正确断开Prisma连接
|
||||
* Disconnect Prisma connection properly on application shutdown
|
||||
*/
|
||||
export async function disconnectPrisma(): Promise<void> {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
739
server/lib/sql-query-builder.ts
Normal file
739
server/lib/sql-query-builder.ts
Normal file
@@ -0,0 +1,739 @@
|
||||
/**
|
||||
* SQL查询构建与执行工具
|
||||
* SQL Query Builder & Executor
|
||||
*
|
||||
* 提供两类能力:
|
||||
* 1) 基础的SQL字符串构建(用于预览)
|
||||
* 2) 基于 Prisma 的查询执行与表字段获取
|
||||
*/
|
||||
|
||||
import type { PrismaClient } from '@prisma/client'
|
||||
import { Prisma } from '@prisma/client'
|
||||
import type { AdvancedQueryOptions } from '@nuxt4crud/shared/types'
|
||||
|
||||
export interface QueryBuilderOptions {
|
||||
table: string
|
||||
select?: string[]
|
||||
where?: Record<string, any>
|
||||
orderBy?: string
|
||||
orderDirection?: 'asc' | 'desc'
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL查询构建器类
|
||||
* SQL Query Builder Class
|
||||
*/
|
||||
// 仅用于生成基础 SQL 预览字符串的构建器(不直接执行)
|
||||
export class SqlQueryBuilder {
|
||||
private options: QueryBuilderOptions
|
||||
|
||||
constructor(options: QueryBuilderOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建SELECT查询
|
||||
* Build SELECT query
|
||||
*/
|
||||
buildSelectQuery(): { sql: string; params: any[] } {
|
||||
const params: any[] = []
|
||||
let sql = 'SELECT '
|
||||
|
||||
// SELECT 子句
|
||||
if (this.options.select && this.options.select.length > 0) {
|
||||
sql += this.options.select.join(', ')
|
||||
} else {
|
||||
sql += '*'
|
||||
}
|
||||
|
||||
// FROM 子句
|
||||
sql += ` FROM ${this.escapeIdentifier(this.options.table)}`
|
||||
|
||||
// WHERE 子句
|
||||
const whereClause = this.buildWhereClause(params)
|
||||
if (whereClause) {
|
||||
sql += ` WHERE ${whereClause}`
|
||||
}
|
||||
|
||||
// ORDER BY 子句
|
||||
if (this.options.orderBy) {
|
||||
sql += ` ORDER BY ${this.escapeIdentifier(this.options.orderBy)} ${this.options.orderDirection || 'ASC'}`
|
||||
}
|
||||
|
||||
// LIMIT 子句
|
||||
if (this.options.limit) {
|
||||
sql += ` LIMIT ${this.options.limit}`
|
||||
}
|
||||
|
||||
// OFFSET 子句
|
||||
if (this.options.offset) {
|
||||
sql += ` OFFSET ${this.options.offset}`
|
||||
}
|
||||
|
||||
return { sql, params }
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建COUNT查询
|
||||
* Build COUNT query
|
||||
*/
|
||||
buildCountQuery(): { sql: string; params: any[] } {
|
||||
const params: any[] = []
|
||||
let sql = `SELECT COUNT(*) as count FROM ${this.escapeIdentifier(this.options.table)}`
|
||||
|
||||
// WHERE 子句
|
||||
const whereClause = this.buildWhereClause(params)
|
||||
if (whereClause) {
|
||||
sql += ` WHERE ${whereClause}`
|
||||
}
|
||||
|
||||
return { sql, params }
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建WHERE子句
|
||||
* Build WHERE clause
|
||||
*/
|
||||
private buildWhereClause(params: any[]): string {
|
||||
if (!this.options.where || Object.keys(this.options.where).length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const conditions: string[] = []
|
||||
|
||||
Object.entries(this.options.where).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
conditions.push(`${this.escapeIdentifier(key)} = ?`)
|
||||
params.push(value)
|
||||
}
|
||||
})
|
||||
|
||||
return conditions.join(' AND ')
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义标识符(防止SQL注入)
|
||||
* Escape identifier (prevent SQL injection)
|
||||
*/
|
||||
private escapeIdentifier(identifier: string): string {
|
||||
// 简单的标识符转义,移除特殊字符
|
||||
return identifier.replace(/[^a-zA-Z0-9_]/g, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分页查询
|
||||
* Get paginated query
|
||||
*/
|
||||
buildPaginatedQueries(): {
|
||||
selectQuery: { sql: string; params: any[] }
|
||||
countQuery: { sql: string; params: any[] }
|
||||
} {
|
||||
return {
|
||||
selectQuery: this.buildSelectQuery(),
|
||||
countQuery: this.buildCountQuery(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prisma 驱动的查询构建器
|
||||
* Prisma-driven query builder and executor
|
||||
*/
|
||||
export class PrismaSqlQueryBuilder {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
/**
|
||||
* 获取当前数据库方言(postgres/mysql/sqlite)
|
||||
* Get current database dialect from DATABASE_URL
|
||||
*/
|
||||
private getDialect(): 'postgres' | 'mysql' | 'sqlite' {
|
||||
const url = process.env.DATABASE_URL || 'file:./dev.db'
|
||||
if (url.startsWith('postgres') || url.includes('postgresql')) return 'postgres'
|
||||
if (url.startsWith('mysql')) return 'mysql'
|
||||
// Prisma sqlite 通常是 file:./xxx.db
|
||||
return 'sqlite'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定表的字段列表(通过数据库系统表)
|
||||
* Get table column names using database system views
|
||||
*/
|
||||
async getTableFields(tableName: string): Promise<string[]> {
|
||||
const dialect = this.getDialect()
|
||||
try {
|
||||
if (dialect === 'postgres') {
|
||||
const rows: Array<{ column_name: string }> = await (this.prisma.$queryRawUnsafe as any)(
|
||||
`SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '${tableName}' ORDER BY ordinal_position;`
|
||||
)
|
||||
return rows.map(r => r.column_name)
|
||||
}
|
||||
|
||||
if (dialect === 'mysql') {
|
||||
const rows: Array<{ Field: string }> = await (this.prisma.$queryRawUnsafe as any)(
|
||||
`SHOW COLUMNS FROM \`${tableName}\`;`
|
||||
)
|
||||
return rows.map(r => r.Field)
|
||||
}
|
||||
|
||||
// sqlite
|
||||
const rows: Array<{ name: string }> = await (this.prisma.$queryRawUnsafe as any)(
|
||||
`PRAGMA table_info('${tableName}');`
|
||||
)
|
||||
return rows.map(r => r.name)
|
||||
} catch (e) {
|
||||
console.warn('getTableFields failed, falling back to defaults:', e)
|
||||
// 尝试返回常见字段集合作为降级方案
|
||||
if (tableName === 'users') {
|
||||
return ['id', 'name', 'email', 'age', 'createdAt', 'updatedAt']
|
||||
}
|
||||
return ['id', 'createdAt', 'updatedAt']
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证高级查询选项的合法性
|
||||
* Validate AdvancedQueryOptions against available fields
|
||||
*/
|
||||
validateQueryOptions(options: AdvancedQueryOptions, availableFields: string[]): string[] {
|
||||
const errors: string[] = []
|
||||
const fieldSet = new Set(availableFields)
|
||||
|
||||
// where 条件校验
|
||||
if (Array.isArray(options.where)) {
|
||||
for (const cond of options.where) {
|
||||
if (!cond || !cond.field) {
|
||||
errors.push('存在缺少字段的条件')
|
||||
continue
|
||||
}
|
||||
if (!fieldSet.has(cond.field)) {
|
||||
errors.push(`字段不存在: ${cond.field}`)
|
||||
}
|
||||
if (!cond.operator) {
|
||||
errors.push(`字段 ${cond.field} 缺少操作符`)
|
||||
}
|
||||
const nullOps = new Set(['is_null', 'is_not_null'])
|
||||
const multiOps = new Set(['in', 'not_in', 'between', 'date_between'])
|
||||
if (!nullOps.has(cond.operator)) {
|
||||
if (multiOps.has(cond.operator)) {
|
||||
if (!Array.isArray(cond.value) || cond.value.length === 0) {
|
||||
errors.push(`字段 ${cond.field} 的操作符 ${cond.operator} 需要数组值`)
|
||||
}
|
||||
} else if (cond.value === undefined || cond.value === null || cond.value === '') {
|
||||
errors.push(`字段 ${cond.field} 的操作符 ${cond.operator} 需要提供值`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 排序校验
|
||||
if (Array.isArray(options.orderBy)) {
|
||||
for (const s of options.orderBy) {
|
||||
if (!s || !s.field) {
|
||||
errors.push('存在缺少字段的排序项')
|
||||
continue
|
||||
}
|
||||
if (!fieldSet.has(s.field)) errors.push(`排序字段不存在: ${s.field}`)
|
||||
const dir = String(s.direction || '').toUpperCase()
|
||||
if (dir !== 'ASC' && dir !== 'DESC') errors.push(`排序方向非法: ${s.direction}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 分页校验
|
||||
const page = Number(options.pagination?.page || 1)
|
||||
const limit = Number(options.pagination?.limit || 20)
|
||||
if (!Number.isFinite(page) || page < 1) errors.push('分页页码必须为正整数')
|
||||
if (!Number.isFinite(limit) || limit < 1 || limit > 1000)
|
||||
errors.push('分页数量必须在1-1000之间')
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
* 将条件映射为 Prisma where 片段
|
||||
* Map one condition to Prisma where fragment
|
||||
*/
|
||||
private mapConditionToPrisma(cond: any): any {
|
||||
const { field, operator, value } = cond || {}
|
||||
|
||||
// 如果值为null、undefined或空字符串(除了isNull/isNotNull操作符),返回空对象
|
||||
if (
|
||||
(value === null || value === undefined || value === '') &&
|
||||
operator !== 'is_null' &&
|
||||
operator !== 'is_not_null'
|
||||
) {
|
||||
return {}
|
||||
}
|
||||
switch (operator) {
|
||||
case 'equals':
|
||||
case 'eq': // 支持简写形式
|
||||
return { [field]: value }
|
||||
case 'not_equals':
|
||||
case 'ne': // 支持简写形式
|
||||
return { [field]: { not: value } }
|
||||
case 'contains':
|
||||
case 'like': // 支持简写形式
|
||||
// 确保值不为空字符串,否则会导致不匹配任何记录
|
||||
if (String(value) === '') return {}
|
||||
return { [field]: { contains: String(value) } }
|
||||
case 'not_contains':
|
||||
case 'notLike': // 支持简写形式
|
||||
// 确保值不为空字符串,否则会导致不匹配任何记录
|
||||
if (String(value) === '') return {}
|
||||
return { [field]: { not: { contains: String(value) } } }
|
||||
case 'starts_with':
|
||||
// 确保值不为空字符串,否则会导致不匹配任何记录
|
||||
if (String(value) === '') return {}
|
||||
return { [field]: { startsWith: String(value) } }
|
||||
case 'ends_with':
|
||||
// 确保值不为空字符串,否则会导致不匹配任何记录
|
||||
if (String(value) === '') return {}
|
||||
return { [field]: { endsWith: String(value) } }
|
||||
case 'gt':
|
||||
case 'greater_than':
|
||||
return { [field]: { gt: value } }
|
||||
case 'gte':
|
||||
case 'greater_than_or_equal':
|
||||
return { [field]: { gte: value } }
|
||||
case 'lt':
|
||||
case 'less_than':
|
||||
return { [field]: { lt: value } }
|
||||
case 'lte':
|
||||
case 'less_than_or_equal':
|
||||
return { [field]: { lte: value } }
|
||||
case 'in': {
|
||||
const arr = Array.isArray(value)
|
||||
? value
|
||||
: String(value)
|
||||
.split(',')
|
||||
.map(v => v.trim())
|
||||
// 确保数组不为空
|
||||
if (arr.length === 0 || (arr.length === 1 && arr[0] === '')) return {}
|
||||
return { [field]: { in: arr } }
|
||||
}
|
||||
case 'not_in': {
|
||||
const arr = Array.isArray(value)
|
||||
? value
|
||||
: String(value)
|
||||
.split(',')
|
||||
.map(v => v.trim())
|
||||
// 确保数组不为空
|
||||
if (arr.length === 0 || (arr.length === 1 && arr[0] === '')) return {}
|
||||
return { [field]: { notIn: arr } }
|
||||
}
|
||||
case 'is_null':
|
||||
return { [field]: null }
|
||||
case 'is_not_null':
|
||||
return { [field]: { not: null } }
|
||||
case 'between': {
|
||||
const [from, to] = Array.isArray(value) ? value : []
|
||||
// 确保from和to都有效
|
||||
if (from === undefined || to === undefined || from === '' || to === '') return {}
|
||||
return { [field]: { gte: from, lte: to } }
|
||||
}
|
||||
case 'date_between': {
|
||||
const [from, to] = Array.isArray(value) ? value : []
|
||||
// 确保from和to都有效
|
||||
if (from === undefined || to === undefined || from === '' || to === '') return {}
|
||||
return { [field]: { gte: new Date(from), lte: new Date(to) } }
|
||||
}
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据查询选项构建 Prisma where 条件及 SQL 预览
|
||||
* Build Prisma where clause and a basic SQL preview from options
|
||||
*/
|
||||
private buildWhereFromOptions(
|
||||
tableName: string,
|
||||
options: AdvancedQueryOptions,
|
||||
searchFields: string[]
|
||||
): { where: any; sqlPreview: string } {
|
||||
const where: any = {}
|
||||
const andConditions: any[] = []
|
||||
const orConditions: any[] = []
|
||||
const sqlPreviewParts: string[] = []
|
||||
|
||||
// 搜索条件
|
||||
const search = options.search?.trim()
|
||||
if (search && searchFields.length > 0) {
|
||||
const searchOr = searchFields.map(f => ({ [f]: { contains: search } }))
|
||||
// 默认与其它条件 AND 组合
|
||||
andConditions.push({ OR: searchOr })
|
||||
sqlPreviewParts.push(`(${searchFields.map(f => `${f} LIKE '%${search}%'`).join(' OR ')})`)
|
||||
}
|
||||
|
||||
// where 条件
|
||||
if (Array.isArray(options.where)) {
|
||||
for (const cond of options.where) {
|
||||
const { field, operator, value } = cond
|
||||
|
||||
// 特殊处理posts表的title字段,因为在Prisma模型中title是必填字段(String)
|
||||
if (tableName === 'posts' && field === 'title') {
|
||||
if (operator === 'isNull' || operator === 'is_null') {
|
||||
// posts表的title字段不可能为null,所以如果有这个条件,直接返回空结果
|
||||
return { where: { id: -1 }, sqlPreview: 'WHERE title IS NULL' }
|
||||
} else if (operator === 'isNotNull' || operator === 'is_not_null') {
|
||||
// posts表的title字段总是不为null,所以这个条件可以忽略
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const frag = this.mapConditionToPrisma(cond)
|
||||
if (!frag || Object.keys(frag).length === 0) continue
|
||||
|
||||
// 普通条件处理
|
||||
if (String(cond.logicalOperator || 'AND').toUpperCase() === 'OR') {
|
||||
orConditions.push(frag)
|
||||
} else {
|
||||
andConditions.push(frag)
|
||||
}
|
||||
|
||||
// 生成SQL预览
|
||||
let sqlPart = ''
|
||||
switch (operator) {
|
||||
case 'equals':
|
||||
sqlPart = `${field} = '${value}'`
|
||||
break
|
||||
case 'not_equals':
|
||||
sqlPart = `${field} <> '${value}'`
|
||||
break
|
||||
case 'contains':
|
||||
sqlPart = `${field} LIKE '%${value}%'`
|
||||
break
|
||||
case 'not_contains':
|
||||
sqlPart = `${field} NOT LIKE '%${value}%'`
|
||||
break
|
||||
case 'starts_with':
|
||||
sqlPart = `${field} LIKE '${value}%'`
|
||||
break
|
||||
case 'ends_with':
|
||||
sqlPart = `${field} LIKE '%${value}'`
|
||||
break
|
||||
case 'gt':
|
||||
sqlPart = `${field} > ${value}`
|
||||
break
|
||||
case 'gte':
|
||||
sqlPart = `${field} >= ${value}`
|
||||
break
|
||||
case 'lt':
|
||||
sqlPart = `${field} < ${value}`
|
||||
break
|
||||
case 'lte':
|
||||
sqlPart = `${field} <= ${value}`
|
||||
break
|
||||
case 'in':
|
||||
sqlPart = `${field} IN (${Array.isArray(value) ? value.map(v => `'${v}'`).join(', ') : `'${value}'`})`
|
||||
break
|
||||
case 'not_in':
|
||||
sqlPart = `${field} NOT IN (${Array.isArray(value) ? value.map(v => `'${v}'`).join(', ') : `'${value}'`})`
|
||||
break
|
||||
case 'is_null':
|
||||
sqlPart = `${field} IS NULL`
|
||||
break
|
||||
case 'is_not_null':
|
||||
sqlPart = `${field} IS NOT NULL`
|
||||
break
|
||||
case 'between':
|
||||
const [from, to] = Array.isArray(value) ? value : []
|
||||
sqlPart = `${field} BETWEEN ${from} AND ${to}`
|
||||
break
|
||||
}
|
||||
if (sqlPart) {
|
||||
if (sqlPreviewParts.length > 0) {
|
||||
sqlPreviewParts.push(String(cond.logicalOperator || 'AND').toUpperCase())
|
||||
}
|
||||
sqlPreviewParts.push(sqlPart)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let finalConditions: any[] = []
|
||||
|
||||
// 处理搜索条件
|
||||
if (search && searchFields.length > 0) {
|
||||
const searchOr = searchFields.map(f => ({ [f]: { contains: search } }))
|
||||
finalConditions.push({ OR: searchOr })
|
||||
}
|
||||
|
||||
// 正确处理逻辑操作符序列
|
||||
// 前端发送的条件数组中,每个条件(除了第一个)都有一个logicalOperator
|
||||
// 表示该条件与前一个条件的连接方式
|
||||
if (Array.isArray(options.where) && options.where.length > 0) {
|
||||
// 第一个条件
|
||||
let currentCondition = this.mapConditionToPrisma(options.where[0])
|
||||
|
||||
// 如果只有一个条件,直接使用它
|
||||
if (options.where.length === 1) {
|
||||
if (currentCondition && Object.keys(currentCondition).length > 0) {
|
||||
finalConditions.push(currentCondition)
|
||||
}
|
||||
} else {
|
||||
// 处理多个条件
|
||||
for (let i = 1; i < options.where.length; i++) {
|
||||
const nextCondition = this.mapConditionToPrisma(options.where[i])
|
||||
const logicalOperator = String(options.where[i].logicalOperator || 'AND').toUpperCase()
|
||||
|
||||
if (!currentCondition || Object.keys(currentCondition).length === 0) {
|
||||
currentCondition = nextCondition
|
||||
continue
|
||||
}
|
||||
|
||||
if (!nextCondition || Object.keys(nextCondition).length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 根据逻辑操作符构建新的条件结构
|
||||
if (logicalOperator === 'OR') {
|
||||
// 如果当前条件已经是OR结构,添加新条件
|
||||
if (currentCondition.OR) {
|
||||
currentCondition.OR.push(nextCondition)
|
||||
} else {
|
||||
// 否则创建新的OR结构
|
||||
currentCondition = {
|
||||
OR: [currentCondition, nextCondition],
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// AND
|
||||
// 如果当前条件已经是AND结构,添加新条件
|
||||
if (currentCondition.AND) {
|
||||
currentCondition.AND.push(nextCondition)
|
||||
} else {
|
||||
// 否则创建新的AND结构
|
||||
currentCondition = {
|
||||
AND: [currentCondition, nextCondition],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentCondition && Object.keys(currentCondition).length > 0) {
|
||||
finalConditions.push(currentCondition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组合所有条件
|
||||
if (finalConditions.length > 1) {
|
||||
where.AND = finalConditions
|
||||
} else if (finalConditions.length === 1) {
|
||||
Object.assign(where, finalConditions[0])
|
||||
}
|
||||
|
||||
// 构建非常基础的 SQL 预览
|
||||
const whereParts: string[] = []
|
||||
if (search && searchFields.length > 0) {
|
||||
const like = search.replace(/'/g, "''")
|
||||
whereParts.push('(' + searchFields.map(f => `${f} ILIKE '%${like}%'`).join(' OR ') + ')')
|
||||
}
|
||||
|
||||
if (Array.isArray(options.where)) {
|
||||
for (const cond of options.where) {
|
||||
const op = String(cond.operator || '').toLowerCase()
|
||||
const f = cond.field
|
||||
const v = cond.value
|
||||
const logic = String(cond.logicalOperator || 'AND').toUpperCase()
|
||||
let exp = ''
|
||||
switch (op) {
|
||||
case 'equals':
|
||||
exp = `${f} = '${v}'`
|
||||
break
|
||||
case 'not_equals':
|
||||
exp = `${f} != '${v}'`
|
||||
break
|
||||
case 'contains':
|
||||
exp = `${f} ILIKE '%${String(v)}%'`
|
||||
break
|
||||
case 'not_contains':
|
||||
exp = `${f} NOT ILIKE '%${String(v)}%'`
|
||||
break
|
||||
case 'starts_with':
|
||||
exp = `${f} ILIKE '${String(v)}%'`
|
||||
break
|
||||
case 'ends_with':
|
||||
exp = `${f} ILIKE '%${String(v)}'`
|
||||
break
|
||||
case 'gt':
|
||||
case 'greater_than':
|
||||
exp = `${f} > ${v}`
|
||||
break
|
||||
case 'gte':
|
||||
case 'greater_than_or_equal':
|
||||
exp = `${f} >= ${v}`
|
||||
break
|
||||
case 'lt':
|
||||
case 'less_than':
|
||||
exp = `${f} < ${v}`
|
||||
break
|
||||
case 'lte':
|
||||
case 'less_than_or_equal':
|
||||
exp = `${f} <= ${v}`
|
||||
break
|
||||
case 'in': {
|
||||
const arr = Array.isArray(v)
|
||||
? v
|
||||
: String(v)
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
exp = `${f} IN (${arr.map(x => `'${x}'`).join(', ')})`
|
||||
break
|
||||
}
|
||||
case 'not_in': {
|
||||
const arr = Array.isArray(v)
|
||||
? v
|
||||
: String(v)
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
exp = `${f} NOT IN (${arr.map(x => `'${x}'`).join(', ')})`
|
||||
break
|
||||
}
|
||||
case 'is_null':
|
||||
exp = `${f} IS NULL`
|
||||
break
|
||||
case 'is_not_null':
|
||||
exp = `${f} IS NOT NULL`
|
||||
break
|
||||
case 'between': {
|
||||
const [from, to] = Array.isArray(v) ? v : []
|
||||
exp = `${f} BETWEEN '${from}' AND '${to}'`
|
||||
break
|
||||
}
|
||||
case 'date_between': {
|
||||
const [from, to] = Array.isArray(v) ? v : []
|
||||
exp = `${f} BETWEEN '${from}' AND '${to}'`
|
||||
break
|
||||
}
|
||||
}
|
||||
if (exp) whereParts.push((whereParts.length > 0 ? logic + ' ' : '') + exp)
|
||||
}
|
||||
}
|
||||
|
||||
const whereSql = whereParts.length > 0 ? ' WHERE ' + whereParts.join(' ') : ''
|
||||
const baseSql = `SELECT * FROM ${tableName}${whereSql}`
|
||||
return { where, sqlPreview: baseSql }
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行普通查询,返回分页数据以及SQL预览
|
||||
* Execute normal query with pagination and a SQL preview
|
||||
*/
|
||||
async executeQuery(
|
||||
tableName: string,
|
||||
options: AdvancedQueryOptions,
|
||||
searchFields: string[]
|
||||
): Promise<{
|
||||
data: any[]
|
||||
total: number
|
||||
pagination: { page: number; limit: number; totalPages: number }
|
||||
executedAt: string
|
||||
table: string
|
||||
query: { sql: string }
|
||||
}> {
|
||||
const page = Number(options.pagination?.page || 1)
|
||||
const limit = Number(options.pagination?.limit || 20)
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
const { where, sqlPreview } = this.buildWhereFromOptions(tableName, options, searchFields)
|
||||
|
||||
// 如果where条件包含id: -1,说明是posts表的title IS NULL查询,直接返回空结果
|
||||
if (where.id === -1) {
|
||||
return {
|
||||
data: [],
|
||||
total: 0,
|
||||
pagination: {
|
||||
page: Number(options.pagination?.page || 1),
|
||||
limit: Number(options.pagination?.limit || 20),
|
||||
totalPages: 0,
|
||||
},
|
||||
executedAt: new Date().toISOString(),
|
||||
table: tableName,
|
||||
query: { sql: sqlPreview },
|
||||
}
|
||||
}
|
||||
|
||||
// 映射到 Prisma 模型名称(简单复数到单数映射)
|
||||
// 将表名转换为Prisma模型名(通常是单数形式)
|
||||
const modelName = tableName === 'users' ? 'user' : tableName === 'posts' ? 'post' : tableName
|
||||
const model = (this.prisma as any)[modelName]
|
||||
|
||||
// 检查模型是否存在
|
||||
if (!model) {
|
||||
const err: any = new Error(`未找到 Prisma 模型: ${modelName}`)
|
||||
err.statusCode = 400
|
||||
throw err
|
||||
}
|
||||
|
||||
// 排序处理
|
||||
const orderBy =
|
||||
Array.isArray(options.orderBy) && options.orderBy.length > 0
|
||||
? options.orderBy.map(s => ({
|
||||
[s.field]: String(s.direction || 'ASC').toLowerCase(),
|
||||
}))
|
||||
: undefined
|
||||
|
||||
// 执行查询
|
||||
const [data, total] = await Promise.all([
|
||||
model.findMany({ where, skip, take: limit, orderBy }),
|
||||
model.count({ where }),
|
||||
])
|
||||
|
||||
return {
|
||||
data,
|
||||
total,
|
||||
pagination: { page, limit, totalPages: Math.max(Math.ceil(total / limit), 1) },
|
||||
executedAt: new Date().toISOString(),
|
||||
table: tableName,
|
||||
query: { sql: sqlPreview + ` ORDER BY ... LIMIT ${limit} OFFSET ${skip}` },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行聚合查询(count/sum/avg/min/max)
|
||||
* Execute aggregate query (count/sum/avg/min/max)
|
||||
*/
|
||||
async executeAggregateQuery(tableName: string, options: AdvancedQueryOptions): Promise<any> {
|
||||
const { where } = this.buildWhereFromOptions(tableName, options, [])
|
||||
|
||||
const modelName = tableName === 'users' ? 'user' : tableName
|
||||
const model = (this.prisma as any)[modelName]
|
||||
if (!model) {
|
||||
const err: any = new Error(`未找到 Prisma 模型: ${modelName}`)
|
||||
err.statusCode = 400
|
||||
throw err
|
||||
}
|
||||
|
||||
const agg = options.aggregate || {}
|
||||
const params: any = { where }
|
||||
|
||||
if (agg.count) params._count = { _all: true }
|
||||
|
||||
const toFieldObj = (fields?: string[]) => {
|
||||
if (!fields || fields.length === 0) return undefined
|
||||
return Object.fromEntries(fields.map(f => [f, true]))
|
||||
}
|
||||
|
||||
params._sum = toFieldObj(agg.sum)
|
||||
params._avg = toFieldObj(agg.avg)
|
||||
params._min = toFieldObj(agg.min)
|
||||
params._max = toFieldObj(agg.max)
|
||||
|
||||
const result = await model.aggregate(params)
|
||||
|
||||
return {
|
||||
executedAt: new Date().toISOString(),
|
||||
result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建基于 Prisma 的查询构建器实例
|
||||
* Create Prisma-based SQL query builder instance
|
||||
*/
|
||||
export function createSqlQueryBuilder(prisma: PrismaClient): PrismaSqlQueryBuilder {
|
||||
return new PrismaSqlQueryBuilder(prisma)
|
||||
}
|
||||
28
server/package.json
Normal file
28
server/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@nuxt4crud/server",
|
||||
"version": "1.0.0",
|
||||
"description": "Nitro server for Nuxt4CRUD",
|
||||
"packageManager": "pnpm@8",
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"pnpm": ">=8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:studio": "prisma studio",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt4crud/shared": "file:../shared",
|
||||
"@prisma/client": "^6.1.0",
|
||||
"ofetch": "^1.4.1",
|
||||
"sql-query-builder": "^0.3.2",
|
||||
"valibot": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.17.6",
|
||||
"prisma": "^6.1.0",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
73
server/plugins/error-handler.ts
Normal file
73
server/plugins/error-handler.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Nitro错误处理插件
|
||||
* Nitro Error Handling Plugin
|
||||
*/
|
||||
import type { ApiResponse } from '@nuxt4crud/shared'
|
||||
|
||||
export default defineNitroPlugin((nitroApp) => {
|
||||
nitroApp.hooks.hook('error', async (error, context) => {
|
||||
console.error('Nitro Error:', error)
|
||||
|
||||
// 验证错误
|
||||
if (error.validationErrors) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: '数据验证失败',
|
||||
data: error.validationErrors,
|
||||
}
|
||||
|
||||
// 设置错误响应
|
||||
if (context.event) {
|
||||
setResponseStatus(context.event, 400)
|
||||
setResponseHeader(context.event, 'Content-Type', 'application/json')
|
||||
await sendWebResponse(context.event, response)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Prisma 错误
|
||||
if (error.code === 'P2002') {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: '数据已存在,请检查唯一性约束',
|
||||
}
|
||||
|
||||
// 设置错误响应
|
||||
if (context.event) {
|
||||
setResponseStatus(context.event, 409)
|
||||
setResponseHeader(context.event, 'Content-Type', 'application/json')
|
||||
await sendWebResponse(context.event, response)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (error.code === 'P2025') {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: '记录不存在',
|
||||
}
|
||||
|
||||
// 设置错误响应
|
||||
if (context.event) {
|
||||
setResponseStatus(context.event, 404)
|
||||
setResponseHeader(context.event, 'Content-Type', 'application/json')
|
||||
await sendWebResponse(context.event, response)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 默认错误处理
|
||||
const statusCode = error.statusCode || 500
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: error.message || '服务器内部错误',
|
||||
}
|
||||
|
||||
// 设置错误响应
|
||||
if (context.event) {
|
||||
setResponseStatus(context.event, statusCode)
|
||||
setResponseHeader(context.event, 'Content-Type', 'application/json')
|
||||
await sendWebResponse(context.event, response)
|
||||
}
|
||||
})
|
||||
})
|
||||
BIN
server/prisma/dev.db
Normal file
BIN
server/prisma/dev.db
Normal file
Binary file not shown.
BIN
server/prisma/dianzhan.db
Normal file
BIN
server/prisma/dianzhan.db
Normal file
Binary file not shown.
12
server/prisma/migrations/20251102134441_init/migration.sql
Normal file
12
server/prisma/migrations/20251102134441_init/migration.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"age" INTEGER,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||
@@ -0,0 +1,8 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "feishu_tokens" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"tenant_access_token" TEXT NOT NULL,
|
||||
"expire" INTEGER NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `feishu_tokens` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropTable
|
||||
PRAGMA foreign_keys=off;
|
||||
DROP TABLE "feishu_tokens";
|
||||
PRAGMA foreign_keys=on;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_tokens" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"access_token" TEXT NOT NULL,
|
||||
"refresh_token" TEXT NOT NULL,
|
||||
"expires_in" INTEGER NOT NULL,
|
||||
"acquired_at" INTEGER NOT NULL,
|
||||
"token_type" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "user_tokens_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_tokens_userId_token_type_key" ON "user_tokens"("userId", "token_type");
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `userId` on the `user_tokens` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_user_tokens" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"access_token" TEXT NOT NULL,
|
||||
"refresh_token" TEXT NOT NULL,
|
||||
"expires_in" INTEGER NOT NULL,
|
||||
"acquired_at" INTEGER NOT NULL,
|
||||
"token_type" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
INSERT INTO "new_user_tokens" ("access_token", "acquired_at", "createdAt", "expires_in", "id", "refresh_token", "token_type", "updatedAt") SELECT "access_token", "acquired_at", "createdAt", "expires_in", "id", "refresh_token", "token_type", "updatedAt" FROM "user_tokens";
|
||||
DROP TABLE "user_tokens";
|
||||
ALTER TABLE "new_user_tokens" RENAME TO "user_tokens";
|
||||
CREATE UNIQUE INDEX "user_tokens_refresh_token_key" ON "user_tokens"("refresh_token");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "posts" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT,
|
||||
"published" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
3
server/prisma/migrations/migration_lock.toml
Normal file
3
server/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "sqlite"
|
||||
43
server/prisma/schema.prisma
Normal file
43
server/prisma/schema.prisma
Normal file
@@ -0,0 +1,43 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
email String @unique
|
||||
age Int?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model UserToken {
|
||||
id Int @id @default(autoincrement())
|
||||
accessToken String @map("access_token")
|
||||
refreshToken String @map("refresh_token") @unique
|
||||
expiresIn Int @map("expires_in")
|
||||
acquiredAt Int @map("acquired_at")
|
||||
tokenType String @map("token_type")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("user_tokens")
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
content String?
|
||||
published Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("posts")
|
||||
}
|
||||
9
server/tsconfig.json
Normal file
9
server/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
8
shared/dist/index.d.ts
vendored
Normal file
8
shared/dist/index.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Shared 包主导出文件
|
||||
* 导出所有类型定义、schemas 和工具函数
|
||||
*/
|
||||
export * from './types/index.js';
|
||||
export { BaseUserSchema, CreateUserSchema, UpdateUserSchema, UserQuerySchema, EmailQuerySchema, UserIdSchema, } from './schemas/index.js';
|
||||
export * from './utils/index.js';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
shared/dist/index.d.ts.map
vendored
Normal file
1
shared/dist/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,cAAc,kBAAkB,CAAA;AAGhC,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,YAAY,GACb,MAAM,oBAAoB,CAAA;AAG3B,cAAc,kBAAkB,CAAA"}
|
||||
11
shared/dist/index.js
vendored
Normal file
11
shared/dist/index.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Shared 包主导出文件
|
||||
* 导出所有类型定义、schemas 和工具函数
|
||||
*/
|
||||
// 导出类型定义
|
||||
export * from './types/index.js';
|
||||
// 导出 schemas(只导出 schema 对象,不导出推断的类型)
|
||||
export { BaseUserSchema, CreateUserSchema, UpdateUserSchema, UserQuerySchema, EmailQuerySchema, UserIdSchema, } from './schemas/index.js';
|
||||
// 导出工具函数
|
||||
export * from './utils/index.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
shared/dist/index.js.map
vendored
Normal file
1
shared/dist/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,SAAS;AACT,cAAc,kBAAkB,CAAA;AAEhC,qCAAqC;AACrC,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,YAAY,GACb,MAAM,oBAAoB,CAAA;AAE3B,SAAS;AACT,cAAc,kBAAkB,CAAA"}
|
||||
6
shared/dist/schemas/index.d.ts
vendored
Normal file
6
shared/dist/schemas/index.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Schemas 导出文件
|
||||
* 导出所有验证 schemas
|
||||
*/
|
||||
export { BaseUserSchema, CreateUserSchema, UpdateUserSchema, UserQuerySchema, EmailQuerySchema, UserIdSchema, } from './user.js';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
shared/dist/schemas/index.d.ts.map
vendored
Normal file
1
shared/dist/schemas/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,YAAY,GACb,MAAM,WAAW,CAAA"}
|
||||
7
shared/dist/schemas/index.js
vendored
Normal file
7
shared/dist/schemas/index.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Schemas 导出文件
|
||||
* 导出所有验证 schemas
|
||||
*/
|
||||
// 只导出 schema 对象,不导出推断的类型(类型在 types 目录中定义)
|
||||
export { BaseUserSchema, CreateUserSchema, UpdateUserSchema, UserQuerySchema, EmailQuerySchema, UserIdSchema, } from './user.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
shared/dist/schemas/index.js.map
vendored
Normal file
1
shared/dist/schemas/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,0CAA0C;AAC1C,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,YAAY,GACb,MAAM,WAAW,CAAA"}
|
||||
63
shared/dist/schemas/user.d.ts
vendored
Normal file
63
shared/dist/schemas/user.d.ts
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 用户相关的 Valibot 验证模式
|
||||
* User-related Valibot validation schemas
|
||||
*/
|
||||
import * as v from 'valibot';
|
||||
/**
|
||||
* 基础用户验证模式
|
||||
* Base user validation schema
|
||||
*/
|
||||
export declare const BaseUserSchema: v.ObjectSchema<{
|
||||
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<"姓名必须是字符串">, v.TrimAction, v.MinLengthAction<string, 2, "姓名至少需要2个字符">, v.MaxLengthAction<string, 50, "姓名不能超过50个字符">, v.NonEmptyAction<string, "姓名不能为空">]>;
|
||||
readonly email: v.SchemaWithPipe<readonly [v.StringSchema<"邮箱必须是字符串">, v.TrimAction, v.EmailAction<string, "请输入有效的邮箱地址">, v.MaxLengthAction<string, 100, "邮箱不能超过100个字符">]>;
|
||||
readonly age: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<"年龄必须是数字">, v.IntegerAction<number, "年龄必须是整数">, v.MinValueAction<number, 1, "年龄不能小于1岁">, v.MaxValueAction<number, 150, "年龄不能超过150岁">]>, undefined>;
|
||||
}, undefined>;
|
||||
/**
|
||||
* 创建用户验证模式
|
||||
* Create user validation schema
|
||||
*/
|
||||
export declare const CreateUserSchema: v.ObjectSchema<{
|
||||
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<"姓名必须是字符串">, v.TrimAction, v.MinLengthAction<string, 2, "姓名至少需要2个字符">, v.MaxLengthAction<string, 50, "姓名不能超过50个字符">, v.NonEmptyAction<string, "姓名不能为空">]>;
|
||||
readonly email: v.SchemaWithPipe<readonly [v.StringSchema<"邮箱必须是字符串">, v.TrimAction, v.EmailAction<string, "请输入有效的邮箱地址">, v.MaxLengthAction<string, 100, "邮箱不能超过100个字符">]>;
|
||||
readonly age: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<"年龄必须是数字">, v.IntegerAction<number, "年龄必须是整数">, v.MinValueAction<number, 1, "年龄不能小于1岁">, v.MaxValueAction<number, 150, "年龄不能超过150岁">]>, undefined>;
|
||||
}, undefined>;
|
||||
/**
|
||||
* 更新用户验证模式 - 所有字段都是可选的
|
||||
* Update user validation schema - all fields are optional
|
||||
*/
|
||||
export declare const UpdateUserSchema: v.ObjectSchema<{
|
||||
readonly name: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<"姓名必须是字符串">, v.TrimAction, v.MinLengthAction<string, 2, "姓名至少需要2个字符">, v.MaxLengthAction<string, 50, "姓名不能超过50个字符">, v.NonEmptyAction<string, "姓名不能为空">]>, undefined>;
|
||||
readonly email: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<"邮箱必须是字符串">, v.TrimAction, v.EmailAction<string, "请输入有效的邮箱地址">, v.MaxLengthAction<string, 100, "邮箱不能超过100个字符">]>, undefined>;
|
||||
readonly age: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<"年龄必须是数字">, v.IntegerAction<number, "年龄必须是整数">, v.MinValueAction<number, 1, "年龄不能小于1岁">, v.MaxValueAction<number, 150, "年龄不能超过150岁">]>, undefined>;
|
||||
}, undefined>;
|
||||
/**
|
||||
* 用户查询参数验证模式
|
||||
* User query parameters validation schema
|
||||
*/
|
||||
export declare const UserQuerySchema: v.ObjectSchema<{
|
||||
readonly page: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<"页码必须是数字">, v.IntegerAction<number, "页码必须是整数">, v.MinValueAction<number, 1, "页码不能小于1">]>, 1>;
|
||||
readonly limit: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<"每页数量必须是数字">, v.IntegerAction<number, "每页数量必须是整数">, v.MinValueAction<number, 1, "每页数量不能小于1">, v.MaxValueAction<number, 100, "每页数量不能超过100">]>, 10>;
|
||||
readonly search: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<"搜索关键词必须是字符串">, v.TrimAction, v.MaxLengthAction<string, 100, "搜索关键词不能超过100个字符">]>, undefined>;
|
||||
readonly minAge: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<"最小年龄必须是数字">, v.IntegerAction<number, "最小年龄必须是整数">, v.MinValueAction<number, 1, "最小年龄不能小于1岁">]>, undefined>;
|
||||
readonly maxAge: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<"最大年龄必须是数字">, v.IntegerAction<number, "最大年龄必须是整数">, v.MaxValueAction<number, 150, "最大年龄不能超过150岁">]>, undefined>;
|
||||
}, undefined>;
|
||||
/**
|
||||
* 邮箱查询验证模式
|
||||
* Email query validation schema
|
||||
*/
|
||||
export declare const EmailQuerySchema: v.ObjectSchema<{
|
||||
readonly email: v.SchemaWithPipe<readonly [v.StringSchema<"邮箱必须是字符串">, v.TrimAction, v.EmailAction<string, "请输入有效的邮箱地址">, v.NonEmptyAction<string, "邮箱不能为空">]>;
|
||||
}, undefined>;
|
||||
/**
|
||||
* ID 参数验证模式
|
||||
* ID parameter validation schema
|
||||
*/
|
||||
export declare const UserIdSchema: v.ObjectSchema<{
|
||||
readonly id: v.SchemaWithPipe<readonly [v.NumberSchema<"用户ID必须是数字">, v.IntegerAction<number, "用户ID必须是整数">, v.MinValueAction<number, 1, "用户ID必须大于0">]>;
|
||||
}, undefined>;
|
||||
export type CreateUserInput = v.InferInput<typeof CreateUserSchema>;
|
||||
export type UpdateUserInput = v.InferInput<typeof UpdateUserSchema>;
|
||||
export type UserQueryParams = v.InferInput<typeof UserQuerySchema>;
|
||||
export type EmailQuery = v.InferInput<typeof EmailQuerySchema>;
|
||||
export type UserIdParams = v.InferInput<typeof UserIdSchema>;
|
||||
//# sourceMappingURL=user.d.ts.map
|
||||
1
shared/dist/schemas/user.d.ts.map
vendored
Normal file
1
shared/dist/schemas/user.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/schemas/user.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAE5B;;;GAGG;AACH,eAAO,MAAM,cAAc;;;;aAsBzB,CAAA;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB;;;;aAAiB,CAAA;AAE9C;;;GAGG;AACH,eAAO,MAAM,gBAAgB;;;;aA0B3B,CAAA;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;aAmC1B,CAAA;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB;;aAO3B,CAAA;AAEF;;;GAGG;AACH,eAAO,MAAM,YAAY;;aAMvB,CAAA;AAGF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAA;AACnE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAA;AACnE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAA;AAClE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAA;AAC9D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAA"}
|
||||
54
shared/dist/schemas/user.js
vendored
Normal file
54
shared/dist/schemas/user.js
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 用户相关的 Valibot 验证模式
|
||||
* User-related Valibot validation schemas
|
||||
*/
|
||||
import * as v from 'valibot';
|
||||
/**
|
||||
* 基础用户验证模式
|
||||
* Base user validation schema
|
||||
*/
|
||||
export const BaseUserSchema = v.object({
|
||||
name: v.pipe(v.string('姓名必须是字符串'), v.trim(), v.minLength(2, '姓名至少需要2个字符'), v.maxLength(50, '姓名不能超过50个字符'), v.nonEmpty('姓名不能为空')),
|
||||
email: v.pipe(v.string('邮箱必须是字符串'), v.trim(), v.email('请输入有效的邮箱地址'), v.maxLength(100, '邮箱不能超过100个字符')),
|
||||
age: v.optional(v.pipe(v.number('年龄必须是数字'), v.integer('年龄必须是整数'), v.minValue(1, '年龄不能小于1岁'), v.maxValue(150, '年龄不能超过150岁'))),
|
||||
});
|
||||
/**
|
||||
* 创建用户验证模式
|
||||
* Create user validation schema
|
||||
*/
|
||||
export const CreateUserSchema = BaseUserSchema;
|
||||
/**
|
||||
* 更新用户验证模式 - 所有字段都是可选的
|
||||
* Update user validation schema - all fields are optional
|
||||
*/
|
||||
export const UpdateUserSchema = v.object({
|
||||
name: v.optional(v.pipe(v.string('姓名必须是字符串'), v.trim(), v.minLength(2, '姓名至少需要2个字符'), v.maxLength(50, '姓名不能超过50个字符'), v.nonEmpty('姓名不能为空'))),
|
||||
email: v.optional(v.pipe(v.string('邮箱必须是字符串'), v.trim(), v.email('请输入有效的邮箱地址'), v.maxLength(100, '邮箱不能超过100个字符'))),
|
||||
age: v.optional(v.pipe(v.number('年龄必须是数字'), v.integer('年龄必须是整数'), v.minValue(1, '年龄不能小于1岁'), v.maxValue(150, '年龄不能超过150岁'))),
|
||||
});
|
||||
/**
|
||||
* 用户查询参数验证模式
|
||||
* User query parameters validation schema
|
||||
*/
|
||||
export const UserQuerySchema = v.object({
|
||||
page: v.optional(v.pipe(v.number('页码必须是数字'), v.integer('页码必须是整数'), v.minValue(1, '页码不能小于1')), 1),
|
||||
limit: v.optional(v.pipe(v.number('每页数量必须是数字'), v.integer('每页数量必须是整数'), v.minValue(1, '每页数量不能小于1'), v.maxValue(100, '每页数量不能超过100')), 10),
|
||||
search: v.optional(v.pipe(v.string('搜索关键词必须是字符串'), v.trim(), v.maxLength(100, '搜索关键词不能超过100个字符'))),
|
||||
minAge: v.optional(v.pipe(v.number('最小年龄必须是数字'), v.integer('最小年龄必须是整数'), v.minValue(1, '最小年龄不能小于1岁'))),
|
||||
maxAge: v.optional(v.pipe(v.number('最大年龄必须是数字'), v.integer('最大年龄必须是整数'), v.maxValue(150, '最大年龄不能超过150岁'))),
|
||||
});
|
||||
/**
|
||||
* 邮箱查询验证模式
|
||||
* Email query validation schema
|
||||
*/
|
||||
export const EmailQuerySchema = v.object({
|
||||
email: v.pipe(v.string('邮箱必须是字符串'), v.trim(), v.email('请输入有效的邮箱地址'), v.nonEmpty('邮箱不能为空')),
|
||||
});
|
||||
/**
|
||||
* ID 参数验证模式
|
||||
* ID parameter validation schema
|
||||
*/
|
||||
export const UserIdSchema = v.object({
|
||||
id: v.pipe(v.number('用户ID必须是数字'), v.integer('用户ID必须是整数'), v.minValue(1, '用户ID必须大于0')),
|
||||
});
|
||||
//# sourceMappingURL=user.js.map
|
||||
1
shared/dist/schemas/user.js.map
vendored
Normal file
1
shared/dist/schemas/user.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../src/schemas/user.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAE5B;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,IAAI,EAAE,CAAC,CAAC,IAAI,CACV,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,EACpB,CAAC,CAAC,IAAI,EAAE,EACR,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,YAAY,CAAC,EAC5B,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,aAAa,CAAC,EAC9B,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CACrB;IACD,KAAK,EAAE,CAAC,CAAC,IAAI,CACX,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,EACpB,CAAC,CAAC,IAAI,EAAE,EACR,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,EACrB,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,cAAc,CAAC,CACjC;IACD,GAAG,EAAE,CAAC,CAAC,QAAQ,CACb,CAAC,CAAC,IAAI,CACJ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,EACnB,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,EACpB,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAC,EACzB,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC,CAC9B,CACF;CACF,CAAC,CAAA;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,cAAc,CAAA;AAE9C;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,IAAI,EAAE,CAAC,CAAC,QAAQ,CACd,CAAC,CAAC,IAAI,CACJ,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,EACpB,CAAC,CAAC,IAAI,EAAE,EACR,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,YAAY,CAAC,EAC5B,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,aAAa,CAAC,EAC9B,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CACrB,CACF;IACD,KAAK,EAAE,CAAC,CAAC,QAAQ,CACf,CAAC,CAAC,IAAI,CACJ,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,EACpB,CAAC,CAAC,IAAI,EAAE,EACR,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,EACrB,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,cAAc,CAAC,CACjC,CACF;IACD,GAAG,EAAE,CAAC,CAAC,QAAQ,CACb,CAAC,CAAC,IAAI,CACJ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,EACnB,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,EACpB,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAC,EACzB,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC,CAC9B,CACF;CACF,CAAC,CAAA;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,IAAI,EAAE,CAAC,CAAC,QAAQ,CACd,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,EAC3E,CAAC,CACF;IACD,KAAK,EAAE,CAAC,CAAC,QAAQ,CACf,CAAC,CAAC,IAAI,CACJ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,EACrB,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,EACtB,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,WAAW,CAAC,EAC1B,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC,CAC/B,EACD,EAAE,CACH;IACD,MAAM,EAAE,CAAC,CAAC,QAAQ,CAChB,CAAC,CAAC,IAAI,CACJ,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,EACvB,CAAC,CAAC,IAAI,EAAE,EACR,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,iBAAiB,CAAC,CACpC,CACF;IACD,MAAM,EAAE,CAAC,CAAC,QAAQ,CAChB,CAAC,CAAC,IAAI,CACJ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,EACrB,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,EACtB,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,YAAY,CAAC,CAC5B,CACF;IACD,MAAM,EAAE,CAAC,CAAC,QAAQ,CAChB,CAAC,CAAC,IAAI,CACJ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,EACrB,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,EACtB,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAC,CAChC,CACF;CACF,CAAC,CAAA;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,KAAK,EAAE,CAAC,CAAC,IAAI,CACX,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,EACpB,CAAC,CAAC,IAAI,EAAE,EACR,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,EACrB,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CACrB;CACF,CAAC,CAAA;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,EAAE,EAAE,CAAC,CAAC,IAAI,CACR,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,EACrB,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,EACtB,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,WAAW,CAAC,CAC3B;CACF,CAAC,CAAA"}
|
||||
85
shared/dist/types/base.d.ts
vendored
Normal file
85
shared/dist/types/base.d.ts
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 基础类型定义
|
||||
* Base type definitions for the application
|
||||
*/
|
||||
/**
|
||||
* 基础实体接口 - 所有实体都应该有的基础字段
|
||||
* Base entity interface - common fields for all entities
|
||||
*/
|
||||
export interface BaseEntity {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
/**
|
||||
* API 响应基础结构
|
||||
* Base API response structure
|
||||
*/
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
/**
|
||||
* 分页查询参数
|
||||
* Pagination query parameters
|
||||
*/
|
||||
export interface PaginationParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
/**
|
||||
* 分页响应结构
|
||||
* Paginated response structure
|
||||
*/
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 查询操作符枚举
|
||||
* Query operators enum
|
||||
*/
|
||||
export declare enum QueryOperator {
|
||||
EQUALS = "equals",
|
||||
NOT_EQUALS = "not_equals",
|
||||
CONTAINS = "contains",
|
||||
NOT_CONTAINS = "not_contains",
|
||||
STARTS_WITH = "starts_with",
|
||||
ENDS_WITH = "ends_with",
|
||||
GREATER_THAN = "gt",
|
||||
GREATER_THAN_OR_EQUAL = "gte",
|
||||
LESS_THAN = "lt",
|
||||
LESS_THAN_OR_EQUAL = "lte",
|
||||
IN = "in",
|
||||
NOT_IN = "not_in",
|
||||
IS_NULL = "is_null",
|
||||
IS_NOT_NULL = "is_not_null"
|
||||
}
|
||||
/**
|
||||
* 查询条件接口
|
||||
* Query condition interface
|
||||
*/
|
||||
export interface QueryCondition {
|
||||
field: string;
|
||||
operator: QueryOperator;
|
||||
value?: any;
|
||||
}
|
||||
/**
|
||||
* 通用查询参数
|
||||
* Universal query parameters
|
||||
*/
|
||||
export interface UniversalQueryParams extends PaginationParams {
|
||||
conditions?: QueryCondition[];
|
||||
orderBy?: {
|
||||
field: string;
|
||||
direction: 'asc' | 'desc';
|
||||
}[];
|
||||
}
|
||||
//# sourceMappingURL=base.d.ts.map
|
||||
1
shared/dist/types/base.d.ts.map
vendored
Normal file
1
shared/dist/types/base.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/types/base.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,EAAE,IAAI,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,GAAG;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,CAAC,EAAE,CAAC,CAAA;IACR,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,UAAU,EAAE;QACV,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,CAAA;QACb,UAAU,EAAE,MAAM,CAAA;KACnB,CAAA;CACF;AAED;;;GAGG;AACH,oBAAY,aAAa;IACvB,MAAM,WAAW;IACjB,UAAU,eAAe;IACzB,QAAQ,aAAa;IACrB,YAAY,iBAAiB;IAC7B,WAAW,gBAAgB;IAC3B,SAAS,cAAc;IACvB,YAAY,OAAO;IACnB,qBAAqB,QAAQ;IAC7B,SAAS,OAAO;IAChB,kBAAkB,QAAQ;IAC1B,EAAE,OAAO;IACT,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,WAAW,gBAAgB;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,aAAa,CAAA;IACvB,KAAK,CAAC,EAAE,GAAG,CAAA;CACZ;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAqB,SAAQ,gBAAgB;IAC5D,UAAU,CAAC,EAAE,cAAc,EAAE,CAAA;IAC7B,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,MAAM,CAAA;QACb,SAAS,EAAE,KAAK,GAAG,MAAM,CAAA;KAC1B,EAAE,CAAA;CACJ"}
|
||||
26
shared/dist/types/base.js
vendored
Normal file
26
shared/dist/types/base.js
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 基础类型定义
|
||||
* Base type definitions for the application
|
||||
*/
|
||||
/**
|
||||
* 查询操作符枚举
|
||||
* Query operators enum
|
||||
*/
|
||||
export var QueryOperator;
|
||||
(function (QueryOperator) {
|
||||
QueryOperator["EQUALS"] = "equals";
|
||||
QueryOperator["NOT_EQUALS"] = "not_equals";
|
||||
QueryOperator["CONTAINS"] = "contains";
|
||||
QueryOperator["NOT_CONTAINS"] = "not_contains";
|
||||
QueryOperator["STARTS_WITH"] = "starts_with";
|
||||
QueryOperator["ENDS_WITH"] = "ends_with";
|
||||
QueryOperator["GREATER_THAN"] = "gt";
|
||||
QueryOperator["GREATER_THAN_OR_EQUAL"] = "gte";
|
||||
QueryOperator["LESS_THAN"] = "lt";
|
||||
QueryOperator["LESS_THAN_OR_EQUAL"] = "lte";
|
||||
QueryOperator["IN"] = "in";
|
||||
QueryOperator["NOT_IN"] = "not_in";
|
||||
QueryOperator["IS_NULL"] = "is_null";
|
||||
QueryOperator["IS_NOT_NULL"] = "is_not_null";
|
||||
})(QueryOperator || (QueryOperator = {}));
|
||||
//# sourceMappingURL=base.js.map
|
||||
1
shared/dist/types/base.js.map
vendored
Normal file
1
shared/dist/types/base.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"base.js","sourceRoot":"","sources":["../../src/types/base.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA8CH;;;GAGG;AACH,MAAM,CAAN,IAAY,aAeX;AAfD,WAAY,aAAa;IACvB,kCAAiB,CAAA;IACjB,0CAAyB,CAAA;IACzB,sCAAqB,CAAA;IACrB,8CAA6B,CAAA;IAC7B,4CAA2B,CAAA;IAC3B,wCAAuB,CAAA;IACvB,oCAAmB,CAAA;IACnB,8CAA6B,CAAA;IAC7B,iCAAgB,CAAA;IAChB,2CAA0B,CAAA;IAC1B,0BAAS,CAAA;IACT,kCAAiB,CAAA;IACjB,oCAAmB,CAAA;IACnB,4CAA2B,CAAA;AAC7B,CAAC,EAfW,aAAa,KAAb,aAAa,QAexB"}
|
||||
8
shared/dist/types/index.d.ts
vendored
Normal file
8
shared/dist/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 类型定义导出索引
|
||||
* Types export index
|
||||
*/
|
||||
export * from './base.js';
|
||||
export * from './post.js';
|
||||
export * from './user.js';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
shared/dist/types/index.d.ts.map
vendored
Normal file
1
shared/dist/types/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA"}
|
||||
8
shared/dist/types/index.js
vendored
Normal file
8
shared/dist/types/index.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 类型定义导出索引
|
||||
* Types export index
|
||||
*/
|
||||
export * from './base.js';
|
||||
export * from './post.js';
|
||||
export * from './user.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
shared/dist/types/index.js.map
vendored
Normal file
1
shared/dist/types/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA"}
|
||||
23
shared/dist/types/post.d.ts
vendored
Normal file
23
shared/dist/types/post.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Post 类型定义
|
||||
* 基于 Prisma 生成的类型
|
||||
*/
|
||||
export interface Post {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string | null;
|
||||
published: boolean;
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
}
|
||||
export interface CreatePostInput {
|
||||
title: string;
|
||||
content?: string;
|
||||
published?: boolean;
|
||||
}
|
||||
export interface UpdatePostInput {
|
||||
title?: string;
|
||||
content?: string;
|
||||
published?: boolean;
|
||||
}
|
||||
//# sourceMappingURL=post.d.ts.map
|
||||
1
shared/dist/types/post.d.ts.map
vendored
Normal file
1
shared/dist/types/post.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"post.d.ts","sourceRoot":"","sources":["../../src/types/post.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,SAAS,EAAE,OAAO,CAAA;IAClB,SAAS,EAAE,IAAI,GAAG,MAAM,CAAA;IACxB,SAAS,EAAE,IAAI,GAAG,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB"}
|
||||
6
shared/dist/types/post.js
vendored
Normal file
6
shared/dist/types/post.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Post 类型定义
|
||||
* 基于 Prisma 生成的类型
|
||||
*/
|
||||
export {};
|
||||
//# sourceMappingURL=post.js.map
|
||||
1
shared/dist/types/post.js.map
vendored
Normal file
1
shared/dist/types/post.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"post.js","sourceRoot":"","sources":["../../src/types/post.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
||||
58
shared/dist/types/user.d.ts
vendored
Normal file
58
shared/dist/types/user.d.ts
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 用户相关类型定义
|
||||
* User-related type definitions
|
||||
*/
|
||||
import type { BaseEntity } from './base.js';
|
||||
/**
|
||||
* 用户实体接口
|
||||
* User entity interface
|
||||
*/
|
||||
export interface User extends BaseEntity {
|
||||
name: string;
|
||||
email: string;
|
||||
age?: number;
|
||||
}
|
||||
/**
|
||||
* 创建用户输入类型
|
||||
* Create user input type
|
||||
*/
|
||||
export interface CreateUserInput {
|
||||
name: string;
|
||||
email: string;
|
||||
age?: number;
|
||||
}
|
||||
/**
|
||||
* 更新用户输入类型
|
||||
* Update user input type
|
||||
*/
|
||||
export interface UpdateUserInput {
|
||||
name?: string;
|
||||
email?: string;
|
||||
age?: number;
|
||||
}
|
||||
/**
|
||||
* 用户查询参数类型
|
||||
* User query parameters type
|
||||
*/
|
||||
export interface UserQueryParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
minAge?: number;
|
||||
maxAge?: number;
|
||||
}
|
||||
/**
|
||||
* 邮箱查询参数类型
|
||||
* Email query parameters type
|
||||
*/
|
||||
export interface EmailQuery {
|
||||
email: string;
|
||||
}
|
||||
/**
|
||||
* 用户ID参数类型
|
||||
* User ID parameters type
|
||||
*/
|
||||
export interface UserIdParams {
|
||||
id: number;
|
||||
}
|
||||
//# sourceMappingURL=user.d.ts.map
|
||||
1
shared/dist/types/user.d.ts.map
vendored
Normal file
1
shared/dist/types/user.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/types/user.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C;;;GAGG;AACH,MAAM,WAAW,IAAK,SAAQ,UAAU;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;CACX"}
|
||||
6
shared/dist/types/user.js
vendored
Normal file
6
shared/dist/types/user.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 用户相关类型定义
|
||||
* User-related type definitions
|
||||
*/
|
||||
export {};
|
||||
//# sourceMappingURL=user.js.map
|
||||
1
shared/dist/types/user.js.map
vendored
Normal file
1
shared/dist/types/user.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../src/types/user.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
||||
6
shared/dist/utils/index.d.ts
vendored
Normal file
6
shared/dist/utils/index.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Utils 导出索引
|
||||
* Utils export index
|
||||
*/
|
||||
export * from './validation.js';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
shared/dist/utils/index.d.ts.map
vendored
Normal file
1
shared/dist/utils/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,iBAAiB,CAAA"}
|
||||
6
shared/dist/utils/index.js
vendored
Normal file
6
shared/dist/utils/index.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Utils 导出索引
|
||||
* Utils export index
|
||||
*/
|
||||
export * from './validation.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
shared/dist/utils/index.js.map
vendored
Normal file
1
shared/dist/utils/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,iBAAiB,CAAA"}
|
||||
59
shared/dist/utils/validation.d.ts
vendored
Normal file
59
shared/dist/utils/validation.d.ts
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 通用验证工具函数
|
||||
* Common validation utility functions
|
||||
*/
|
||||
import * as v from 'valibot';
|
||||
/**
|
||||
* 验证错误接口
|
||||
* Validation error interface
|
||||
*/
|
||||
export interface ValidationError {
|
||||
field: string;
|
||||
message: string;
|
||||
code: string;
|
||||
}
|
||||
/**
|
||||
* 验证结果接口
|
||||
* Validation result interface
|
||||
*/
|
||||
export interface ValidationResult<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
errors?: ValidationError[];
|
||||
}
|
||||
/**
|
||||
* 将 Valibot 验证错误转换为自定义格式
|
||||
* Convert Valibot validation errors to custom format
|
||||
*/
|
||||
export declare function formatValidationErrors(issues: v.BaseIssue<unknown>[]): ValidationError[];
|
||||
/**
|
||||
* 安全解析数据
|
||||
* Safely parse data with validation
|
||||
*/
|
||||
export declare function safeParseData<T>(schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>, data: unknown): ValidationResult<T>;
|
||||
/**
|
||||
* 解析并验证数据,失败时抛出错误
|
||||
* Parse and validate data, throw error on failure
|
||||
*/
|
||||
export declare function parseData<T>(schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>, data: unknown): T;
|
||||
/**
|
||||
* 创建验证中间件函数(用于 API)
|
||||
* Create validation middleware function (for API)
|
||||
*/
|
||||
export declare function createValidationMiddleware<T>(schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>): (data: unknown) => T;
|
||||
/**
|
||||
* 验证查询参数
|
||||
* Validate query parameters
|
||||
*/
|
||||
export declare function validateQuery<T>(schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>, query: Record<string, any>): T;
|
||||
/**
|
||||
* 验证请求体数据
|
||||
* Validate request body data
|
||||
*/
|
||||
export declare function validateBody<T>(schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>, body: unknown): T;
|
||||
/**
|
||||
* 验证路径参数
|
||||
* Validate path parameters
|
||||
*/
|
||||
export declare function validateParams<T>(schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>, params: Record<string, any>): T;
|
||||
//# sourceMappingURL=validation.d.ts.map
|
||||
1
shared/dist/utils/validation.d.ts.map
vendored
Normal file
1
shared/dist/utils/validation.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAE5B;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,CAAC,EAAE,CAAC,CAAA;IACR,MAAM,CAAC,EAAE,eAAe,EAAE,CAAA;CAC3B;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,GAAG,eAAe,EAAE,CAMxF;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,EACtD,IAAI,EAAE,OAAO,GACZ,gBAAgB,CAAC,CAAC,CAAC,CA2BrB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,EACtD,IAAI,EAAE,OAAO,GACZ,CAAC,CAWH;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,EAC1C,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,IAE9C,MAAM,OAAO,KAAG,CAAC,CAG1B;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,EACtD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACzB,CAAC,CAqBH;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAC5B,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,EACtD,IAAI,EAAE,OAAO,GACZ,CAAC,CAEH;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAC9B,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,EACtD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,CAAC,CAeH"}
|
||||
118
shared/dist/utils/validation.js
vendored
Normal file
118
shared/dist/utils/validation.js
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 通用验证工具函数
|
||||
* Common validation utility functions
|
||||
*/
|
||||
import * as v from 'valibot';
|
||||
/**
|
||||
* 将 Valibot 验证错误转换为自定义格式
|
||||
* Convert Valibot validation errors to custom format
|
||||
*/
|
||||
export function formatValidationErrors(issues) {
|
||||
return issues.map(issue => ({
|
||||
field: issue.path?.map(p => p.key).join('.') || 'unknown',
|
||||
message: issue.message,
|
||||
code: issue.type || 'VALIDATION_ERROR',
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* 安全解析数据
|
||||
* Safely parse data with validation
|
||||
*/
|
||||
export function safeParseData(schema, data) {
|
||||
try {
|
||||
const result = v.safeParse(schema, data);
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
data: result.output,
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
success: false,
|
||||
errors: formatValidationErrors(result.issues),
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
errors: [
|
||||
{
|
||||
field: 'unknown',
|
||||
message: error instanceof Error ? error.message : '验证过程中发生未知错误',
|
||||
code: 'UNKNOWN_ERROR',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 解析并验证数据,失败时抛出错误
|
||||
* Parse and validate data, throw error on failure
|
||||
*/
|
||||
export function parseData(schema, data) {
|
||||
const result = safeParseData(schema, data);
|
||||
if (!result.success) {
|
||||
const error = new Error('数据验证失败');
|
||||
error.statusCode = 400;
|
||||
error.validationErrors = result.errors;
|
||||
throw error;
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
/**
|
||||
* 创建验证中间件函数(用于 API)
|
||||
* Create validation middleware function (for API)
|
||||
*/
|
||||
export function createValidationMiddleware(schema) {
|
||||
return (data) => {
|
||||
return parseData(schema, data);
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 验证查询参数
|
||||
* Validate query parameters
|
||||
*/
|
||||
export function validateQuery(schema, query) {
|
||||
// 转换查询参数中的数字字符串
|
||||
const processedQuery = Object.entries(query).reduce((acc, [key, value]) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return acc;
|
||||
}
|
||||
// 尝试转换数字
|
||||
if (typeof value === 'string' && !isNaN(Number(value))) {
|
||||
acc[key] = Number(value);
|
||||
}
|
||||
else {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return parseData(schema, processedQuery);
|
||||
}
|
||||
/**
|
||||
* 验证请求体数据
|
||||
* Validate request body data
|
||||
*/
|
||||
export function validateBody(schema, body) {
|
||||
return parseData(schema, body);
|
||||
}
|
||||
/**
|
||||
* 验证路径参数
|
||||
* Validate path parameters
|
||||
*/
|
||||
export function validateParams(schema, params) {
|
||||
// 转换路径参数中的数字字符串
|
||||
const processedParams = Object.entries(params).reduce((acc, [key, value]) => {
|
||||
if (typeof value === 'string' && !isNaN(Number(value))) {
|
||||
acc[key] = Number(value);
|
||||
}
|
||||
else {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return parseData(schema, processedParams);
|
||||
}
|
||||
//# sourceMappingURL=validation.js.map
|
||||
1
shared/dist/utils/validation.js.map
vendored
Normal file
1
shared/dist/utils/validation.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAsB5B;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAA8B;IACnE,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC1B,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,SAAS;QACzD,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,kBAAkB;KACvC,CAAC,CAAC,CAAA;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAsD,EACtD,IAAa;IAEb,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,CAAC,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;QAExC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,IAAI,EAAE,MAAM,CAAC,MAAM;aACpB,CAAA;QACH,CAAC;aAAM,CAAC;YACN,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,MAAM,EAAE,sBAAsB,CAAC,MAAM,CAAC,MAAM,CAAC;aAC9C,CAAA;QACH,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,OAAO,EAAE,KAAK;YACd,MAAM,EAAE;gBACN;oBACE,KAAK,EAAE,SAAS;oBAChB,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa;oBAC/D,IAAI,EAAE,eAAe;iBACtB;aACF;SACF,CAAA;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CACvB,MAAsD,EACtD,IAAa;IAEb,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IAE1C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAQ,CAAA;QACxC,KAAK,CAAC,UAAU,GAAG,GAAG,CAAA;QACtB,KAAK,CAAC,gBAAgB,GAAG,MAAM,CAAC,MAAM,CAAA;QACtC,MAAM,KAAK,CAAA;IACb,CAAC;IAED,OAAO,MAAM,CAAC,IAAK,CAAA;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,0BAA0B,CACxC,MAAsD;IAEtD,OAAO,CAAC,IAAa,EAAK,EAAE;QAC1B,OAAO,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IAChC,CAAC,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAsD,EACtD,KAA0B;IAE1B,gBAAgB;IAChB,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CACjD,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;QACpB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;YAC1D,OAAO,GAAG,CAAA;QACZ,CAAC;QAED,SAAS;QACT,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACvD,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAC1B,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;QAClB,CAAC;QAED,OAAO,GAAG,CAAA;IACZ,CAAC,EACD,EAAyB,CAC1B,CAAA;IAED,OAAO,SAAS,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;AAC1C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAC1B,MAAsD,EACtD,IAAa;IAEb,OAAO,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;AAChC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAsD,EACtD,MAA2B;IAE3B,gBAAgB;IAChB,MAAM,eAAe,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CACnD,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;QACpB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACvD,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAC1B,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;QAClB,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC,EACD,EAAyB,CAC1B,CAAA;IAED,OAAO,SAAS,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;AAC3C,CAAC"}
|
||||
48
shared/package.json
Normal file
48
shared/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@nuxt4crud/shared",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"packageManager": "pnpm@8",
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"pnpm": ">=8.0.0"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./dist/types/index.js",
|
||||
"require": "./dist/types/index.js",
|
||||
"types": "./dist/types/index.d.ts"
|
||||
},
|
||||
"./schemas": {
|
||||
"import": "./dist/schemas/index.js",
|
||||
"require": "./dist/schemas/index.js",
|
||||
"types": "./dist/schemas/index.d.ts"
|
||||
},
|
||||
"./utils": {
|
||||
"import": "./dist/utils/index.js",
|
||||
"require": "./dist/utils/index.js",
|
||||
"types": "./dist/utils/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"clean": "rimraf dist",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"valibot": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.3",
|
||||
"rimraf": "^5.0.10",
|
||||
"@types/node": "^20.17.6"
|
||||
}
|
||||
}
|
||||
20
shared/src/index.ts
Normal file
20
shared/src/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Shared 包主导出文件
|
||||
* 导出所有类型定义、schemas 和工具函数
|
||||
*/
|
||||
|
||||
// 导出类型定义
|
||||
export * from './types/index.js'
|
||||
|
||||
// 导出 schemas(只导出 schema 对象,不导出推断的类型)
|
||||
export {
|
||||
BaseUserSchema,
|
||||
CreateUserSchema,
|
||||
UpdateUserSchema,
|
||||
UserQuerySchema,
|
||||
EmailQuerySchema,
|
||||
UserIdSchema,
|
||||
} from './schemas/index.js'
|
||||
|
||||
// 导出工具函数
|
||||
export * from './utils/index.js'
|
||||
14
shared/src/schemas/index.ts
Normal file
14
shared/src/schemas/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Schemas 导出文件
|
||||
* 导出所有验证 schemas
|
||||
*/
|
||||
|
||||
// 只导出 schema 对象,不导出推断的类型(类型在 types 目录中定义)
|
||||
export {
|
||||
BaseUserSchema,
|
||||
CreateUserSchema,
|
||||
UpdateUserSchema,
|
||||
UserQuerySchema,
|
||||
EmailQuerySchema,
|
||||
UserIdSchema,
|
||||
} from './user.js'
|
||||
145
shared/src/schemas/user.ts
Normal file
145
shared/src/schemas/user.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 用户相关的 Valibot 验证模式
|
||||
* User-related Valibot validation schemas
|
||||
*/
|
||||
|
||||
import * as v from 'valibot'
|
||||
|
||||
/**
|
||||
* 基础用户验证模式
|
||||
* Base user validation schema
|
||||
*/
|
||||
export const BaseUserSchema = v.object({
|
||||
name: v.pipe(
|
||||
v.string('姓名必须是字符串'),
|
||||
v.trim(),
|
||||
v.minLength(2, '姓名至少需要2个字符'),
|
||||
v.maxLength(50, '姓名不能超过50个字符'),
|
||||
v.nonEmpty('姓名不能为空')
|
||||
),
|
||||
email: v.pipe(
|
||||
v.string('邮箱必须是字符串'),
|
||||
v.trim(),
|
||||
v.email('请输入有效的邮箱地址'),
|
||||
v.maxLength(100, '邮箱不能超过100个字符')
|
||||
),
|
||||
age: v.optional(
|
||||
v.pipe(
|
||||
v.number('年龄必须是数字'),
|
||||
v.integer('年龄必须是整数'),
|
||||
v.minValue(1, '年龄不能小于1岁'),
|
||||
v.maxValue(150, '年龄不能超过150岁')
|
||||
)
|
||||
),
|
||||
})
|
||||
|
||||
/**
|
||||
* 创建用户验证模式
|
||||
* Create user validation schema
|
||||
*/
|
||||
export const CreateUserSchema = BaseUserSchema
|
||||
|
||||
/**
|
||||
* 更新用户验证模式 - 所有字段都是可选的
|
||||
* Update user validation schema - all fields are optional
|
||||
*/
|
||||
export const UpdateUserSchema = v.object({
|
||||
name: v.optional(
|
||||
v.pipe(
|
||||
v.string('姓名必须是字符串'),
|
||||
v.trim(),
|
||||
v.minLength(2, '姓名至少需要2个字符'),
|
||||
v.maxLength(50, '姓名不能超过50个字符'),
|
||||
v.nonEmpty('姓名不能为空')
|
||||
)
|
||||
),
|
||||
email: v.optional(
|
||||
v.pipe(
|
||||
v.string('邮箱必须是字符串'),
|
||||
v.trim(),
|
||||
v.email('请输入有效的邮箱地址'),
|
||||
v.maxLength(100, '邮箱不能超过100个字符')
|
||||
)
|
||||
),
|
||||
age: v.optional(
|
||||
v.pipe(
|
||||
v.number('年龄必须是数字'),
|
||||
v.integer('年龄必须是整数'),
|
||||
v.minValue(1, '年龄不能小于1岁'),
|
||||
v.maxValue(150, '年龄不能超过150岁')
|
||||
)
|
||||
),
|
||||
})
|
||||
|
||||
/**
|
||||
* 用户查询参数验证模式
|
||||
* User query parameters validation schema
|
||||
*/
|
||||
export const UserQuerySchema = v.object({
|
||||
page: v.optional(
|
||||
v.pipe(v.number('页码必须是数字'), v.integer('页码必须是整数'), v.minValue(1, '页码不能小于1')),
|
||||
1
|
||||
),
|
||||
limit: v.optional(
|
||||
v.pipe(
|
||||
v.number('每页数量必须是数字'),
|
||||
v.integer('每页数量必须是整数'),
|
||||
v.minValue(1, '每页数量不能小于1'),
|
||||
v.maxValue(100, '每页数量不能超过100')
|
||||
),
|
||||
10
|
||||
),
|
||||
search: v.optional(
|
||||
v.pipe(
|
||||
v.string('搜索关键词必须是字符串'),
|
||||
v.trim(),
|
||||
v.maxLength(100, '搜索关键词不能超过100个字符')
|
||||
)
|
||||
),
|
||||
minAge: v.optional(
|
||||
v.pipe(
|
||||
v.number('最小年龄必须是数字'),
|
||||
v.integer('最小年龄必须是整数'),
|
||||
v.minValue(1, '最小年龄不能小于1岁')
|
||||
)
|
||||
),
|
||||
maxAge: v.optional(
|
||||
v.pipe(
|
||||
v.number('最大年龄必须是数字'),
|
||||
v.integer('最大年龄必须是整数'),
|
||||
v.maxValue(150, '最大年龄不能超过150岁')
|
||||
)
|
||||
),
|
||||
})
|
||||
|
||||
/**
|
||||
* 邮箱查询验证模式
|
||||
* Email query validation schema
|
||||
*/
|
||||
export const EmailQuerySchema = v.object({
|
||||
email: v.pipe(
|
||||
v.string('邮箱必须是字符串'),
|
||||
v.trim(),
|
||||
v.email('请输入有效的邮箱地址'),
|
||||
v.nonEmpty('邮箱不能为空')
|
||||
),
|
||||
})
|
||||
|
||||
/**
|
||||
* ID 参数验证模式
|
||||
* ID parameter validation schema
|
||||
*/
|
||||
export const UserIdSchema = v.object({
|
||||
id: v.pipe(
|
||||
v.number('用户ID必须是数字'),
|
||||
v.integer('用户ID必须是整数'),
|
||||
v.minValue(1, '用户ID必须大于0')
|
||||
),
|
||||
})
|
||||
|
||||
// 导出类型推断
|
||||
export type CreateUserInput = v.InferInput<typeof CreateUserSchema>
|
||||
export type UpdateUserInput = v.InferInput<typeof UpdateUserSchema>
|
||||
export type UserQueryParams = v.InferInput<typeof UserQuerySchema>
|
||||
export type EmailQuery = v.InferInput<typeof EmailQuerySchema>
|
||||
export type UserIdParams = v.InferInput<typeof UserIdSchema>
|
||||
91
shared/src/types/base.ts
Normal file
91
shared/src/types/base.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 基础类型定义
|
||||
* Base type definitions for the application
|
||||
*/
|
||||
|
||||
/**
|
||||
* 基础实体接口 - 所有实体都应该有的基础字段
|
||||
* Base entity interface - common fields for all entities
|
||||
*/
|
||||
export interface BaseEntity {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* API 响应基础结构
|
||||
* Base API response structure
|
||||
*/
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
data?: T
|
||||
message?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询参数
|
||||
* Pagination query parameters
|
||||
*/
|
||||
export interface PaginationParams {
|
||||
page?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应结构
|
||||
* Paginated response structure
|
||||
*/
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[]
|
||||
pagination: {
|
||||
page: number
|
||||
limit: number
|
||||
total: number
|
||||
totalPages: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询操作符枚举
|
||||
* Query operators enum
|
||||
*/
|
||||
export enum QueryOperator {
|
||||
EQUALS = 'equals',
|
||||
NOT_EQUALS = 'not_equals',
|
||||
CONTAINS = 'contains',
|
||||
NOT_CONTAINS = 'not_contains',
|
||||
STARTS_WITH = 'starts_with',
|
||||
ENDS_WITH = 'ends_with',
|
||||
GREATER_THAN = 'gt',
|
||||
GREATER_THAN_OR_EQUAL = 'gte',
|
||||
LESS_THAN = 'lt',
|
||||
LESS_THAN_OR_EQUAL = 'lte',
|
||||
IN = 'in',
|
||||
NOT_IN = 'not_in',
|
||||
IS_NULL = 'is_null',
|
||||
IS_NOT_NULL = 'is_not_null',
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询条件接口
|
||||
* Query condition interface
|
||||
*/
|
||||
export interface QueryCondition {
|
||||
field: string
|
||||
operator: QueryOperator
|
||||
value?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用查询参数
|
||||
* Universal query parameters
|
||||
*/
|
||||
export interface UniversalQueryParams extends PaginationParams {
|
||||
conditions?: QueryCondition[]
|
||||
orderBy?: {
|
||||
field: string
|
||||
direction: 'asc' | 'desc'
|
||||
}[]
|
||||
}
|
||||
9
shared/src/types/index.ts
Normal file
9
shared/src/types/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 类型定义导出索引
|
||||
* Types export index
|
||||
*/
|
||||
|
||||
export * from './base.js'
|
||||
export * from './post.js'
|
||||
export * from './user.js'
|
||||
|
||||
25
shared/src/types/post.ts
Normal file
25
shared/src/types/post.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Post 类型定义
|
||||
* 基于 Prisma 生成的类型
|
||||
*/
|
||||
|
||||
export interface Post {
|
||||
id: number
|
||||
title: string
|
||||
content: string | null
|
||||
published: boolean
|
||||
createdAt: Date | string
|
||||
updatedAt: Date | string
|
||||
}
|
||||
|
||||
export interface CreatePostInput {
|
||||
title: string
|
||||
content?: string
|
||||
published?: boolean
|
||||
}
|
||||
|
||||
export interface UpdatePostInput {
|
||||
title?: string
|
||||
content?: string
|
||||
published?: boolean
|
||||
}
|
||||
64
shared/src/types/user.ts
Normal file
64
shared/src/types/user.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 用户相关类型定义
|
||||
* User-related type definitions
|
||||
*/
|
||||
|
||||
import type { BaseEntity } from './base.js'
|
||||
|
||||
/**
|
||||
* 用户实体接口
|
||||
* User entity interface
|
||||
*/
|
||||
export interface User extends BaseEntity {
|
||||
name: string
|
||||
email: string
|
||||
age?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户输入类型
|
||||
* Create user input type
|
||||
*/
|
||||
export interface CreateUserInput {
|
||||
name: string
|
||||
email: string
|
||||
age?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户输入类型
|
||||
* Update user input type
|
||||
*/
|
||||
export interface UpdateUserInput {
|
||||
name?: string
|
||||
email?: string
|
||||
age?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户查询参数类型
|
||||
* User query parameters type
|
||||
*/
|
||||
export interface UserQueryParams {
|
||||
page?: number
|
||||
limit?: number
|
||||
search?: string
|
||||
minAge?: number
|
||||
maxAge?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱查询参数类型
|
||||
* Email query parameters type
|
||||
*/
|
||||
export interface EmailQuery {
|
||||
email: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户ID参数类型
|
||||
* User ID parameters type
|
||||
*/
|
||||
export interface UserIdParams {
|
||||
id: number
|
||||
}
|
||||
6
shared/src/utils/index.ts
Normal file
6
shared/src/utils/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Utils 导出索引
|
||||
* Utils export index
|
||||
*/
|
||||
|
||||
export * from './validation.js'
|
||||
171
shared/src/utils/validation.ts
Normal file
171
shared/src/utils/validation.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 通用验证工具函数
|
||||
* Common validation utility functions
|
||||
*/
|
||||
|
||||
import * as v from 'valibot'
|
||||
|
||||
/**
|
||||
* 验证错误接口
|
||||
* Validation error interface
|
||||
*/
|
||||
export interface ValidationError {
|
||||
field: string
|
||||
message: string
|
||||
code: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证结果接口
|
||||
* Validation result interface
|
||||
*/
|
||||
export interface ValidationResult<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
errors?: ValidationError[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Valibot 验证错误转换为自定义格式
|
||||
* Convert Valibot validation errors to custom format
|
||||
*/
|
||||
export function formatValidationErrors(issues: v.BaseIssue<unknown>[]): ValidationError[] {
|
||||
return issues.map(issue => ({
|
||||
field: issue.path?.map(p => p.key).join('.') || 'unknown',
|
||||
message: issue.message,
|
||||
code: issue.type || 'VALIDATION_ERROR',
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全解析数据
|
||||
* Safely parse data with validation
|
||||
*/
|
||||
export function safeParseData<T>(
|
||||
schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>,
|
||||
data: unknown
|
||||
): ValidationResult<T> {
|
||||
try {
|
||||
const result = v.safeParse(schema, data)
|
||||
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
data: result.output,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
errors: formatValidationErrors(result.issues),
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
errors: [
|
||||
{
|
||||
field: 'unknown',
|
||||
message: error instanceof Error ? error.message : '验证过程中发生未知错误',
|
||||
code: 'UNKNOWN_ERROR',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析并验证数据,失败时抛出错误
|
||||
* Parse and validate data, throw error on failure
|
||||
*/
|
||||
export function parseData<T>(
|
||||
schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>,
|
||||
data: unknown
|
||||
): T {
|
||||
const result = safeParseData(schema, data)
|
||||
|
||||
if (!result.success) {
|
||||
const error = new Error('数据验证失败') as any
|
||||
error.statusCode = 400
|
||||
error.validationErrors = result.errors
|
||||
throw error
|
||||
}
|
||||
|
||||
return result.data!
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建验证中间件函数(用于 API)
|
||||
* Create validation middleware function (for API)
|
||||
*/
|
||||
export function createValidationMiddleware<T>(
|
||||
schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>
|
||||
) {
|
||||
return (data: unknown): T => {
|
||||
return parseData(schema, data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证查询参数
|
||||
* Validate query parameters
|
||||
*/
|
||||
export function validateQuery<T>(
|
||||
schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>,
|
||||
query: Record<string, any>
|
||||
): T {
|
||||
// 转换查询参数中的数字字符串
|
||||
const processedQuery = Object.entries(query).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return acc
|
||||
}
|
||||
|
||||
// 尝试转换数字
|
||||
if (typeof value === 'string' && !isNaN(Number(value))) {
|
||||
acc[key] = Number(value)
|
||||
} else {
|
||||
acc[key] = value
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any>
|
||||
)
|
||||
|
||||
return parseData(schema, processedQuery)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证请求体数据
|
||||
* Validate request body data
|
||||
*/
|
||||
export function validateBody<T>(
|
||||
schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>,
|
||||
body: unknown
|
||||
): T {
|
||||
return parseData(schema, body)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证路径参数
|
||||
* Validate path parameters
|
||||
*/
|
||||
export function validateParams<T>(
|
||||
schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>,
|
||||
params: Record<string, any>
|
||||
): T {
|
||||
// 转换路径参数中的数字字符串
|
||||
const processedParams = Object.entries(params).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (typeof value === 'string' && !isNaN(Number(value))) {
|
||||
acc[key] = Number(value)
|
||||
} else {
|
||||
acc[key] = value
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any>
|
||||
)
|
||||
|
||||
return parseData(schema, processedParams)
|
||||
}
|
||||
9
shared/tsconfig.json
Normal file
9
shared/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
14
shared/types/base.ts
Normal file
14
shared/types/base.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 基础类型定义
|
||||
* Base type definitions
|
||||
*/
|
||||
|
||||
/**
|
||||
* 基础实体接口 - 所有实体都应该有的基础字段
|
||||
* Base entity interface - common fields for all entities
|
||||
*/
|
||||
export interface BaseEntity {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
129
shared/types/index.ts
Normal file
129
shared/types/index.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 类型定义统一导出
|
||||
* Unified type definitions export
|
||||
*
|
||||
* 共享类型定义文件
|
||||
* 用于前后端类型一致性
|
||||
*/
|
||||
|
||||
// 基础类型
|
||||
export * from './base'
|
||||
|
||||
// 用户相关类型
|
||||
export * from './user'
|
||||
|
||||
// 飞书相关类型
|
||||
export * from './feishu'
|
||||
|
||||
// 上海CIM相关类型
|
||||
export * from './cim'
|
||||
|
||||
// 验证相关类型和工具
|
||||
export * from '../src/schemas/user'
|
||||
export * from '../utils/validation'
|
||||
|
||||
// ==================== API 响应类型 ====================
|
||||
|
||||
/**
|
||||
* API 响应类型
|
||||
* @template T 响应数据类型
|
||||
*/
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
data?: T
|
||||
message?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页参数类型
|
||||
*/
|
||||
export interface PaginationParams {
|
||||
page?: number
|
||||
limit?: number
|
||||
search?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应类型
|
||||
* @template T 数据项类型
|
||||
*/
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单验证规则类型
|
||||
*/
|
||||
export interface FormRule {
|
||||
required?: boolean
|
||||
message?: string
|
||||
trigger?: string | string[]
|
||||
min?: number
|
||||
max?: number
|
||||
pattern?: RegExp
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格列配置类型
|
||||
*/
|
||||
export interface TableColumn {
|
||||
prop: string
|
||||
label: string
|
||||
width?: string | number
|
||||
sortable?: boolean
|
||||
formatter?: (row: any, column: any, cellValue: any) => string
|
||||
}
|
||||
|
||||
// ==================== 通用 CRUD 类型定义 ====================
|
||||
|
||||
/**
|
||||
* 通用查询参数接口
|
||||
*/
|
||||
export interface BaseQueryParams extends PaginationParams {
|
||||
orderBy?: string
|
||||
orderDirection?: 'asc' | 'desc'
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用创建输入类型
|
||||
*/
|
||||
export interface BaseCreateInput {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用更新输入类型
|
||||
*/
|
||||
export interface BaseUpdateInput {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* CRUD 操作选项
|
||||
*/
|
||||
export interface CrudOptions {
|
||||
// 是否启用软删除
|
||||
softDelete?: boolean
|
||||
// 默认排序字段
|
||||
defaultOrderBy?: string
|
||||
// 默认排序方向
|
||||
defaultOrderDirection?: 'asc' | 'desc'
|
||||
// 可搜索字段
|
||||
searchableFields?: string[]
|
||||
// 唯一字段验证
|
||||
uniqueFields?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展的API响应类型,包含验证错误
|
||||
* @template T 响应数据类型
|
||||
*/
|
||||
export interface ExtendedApiResponse<T = any> extends ApiResponse<T> {
|
||||
validationErrors?: ValidationError[]
|
||||
}
|
||||
19
shared/types/user.ts
Normal file
19
shared/types/user.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 用户相关类型定义
|
||||
* User-related type definitions
|
||||
*/
|
||||
|
||||
import { BaseEntity } from './base'
|
||||
|
||||
/**
|
||||
* 用户实体接口
|
||||
* User entity interface
|
||||
*/
|
||||
export interface User extends BaseEntity {
|
||||
name: string
|
||||
email: string
|
||||
age: number | null
|
||||
}
|
||||
|
||||
// 注意:CreateUserInput 和 UpdateUserInput 类型现在在 ~/shared/schemas/user.ts 中定义
|
||||
// 请从 ~/shared/schemas/user 导入这些类型
|
||||
Reference in New Issue
Block a user