This commit is contained in:
ch197511161
2025-12-11 02:09:07 +08:00
parent 54d6acbce3
commit aaaf08e8f3
84 changed files with 4131 additions and 0 deletions

99
.gitignore vendored Normal file
View 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

View 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
View 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
View 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
View 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"
]
}

View 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
View 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',
}

View 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
View File

@@ -0,0 +1 @@
DATABASE_URL=file:./dev.db

3
server/.npmrc Normal file
View File

@@ -0,0 +1,3 @@
legacy-peer-deps=true
fund=false
audit=false

View 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)
})

View 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)
})

View 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)
})

View 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)
})

View 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)
})

View 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 : '未知错误'}`,
})
}
})

View 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 : '未知错误'}`,
})
}
})

View 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
View 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
View 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
View 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()
}

View 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
View 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"
}
}

View 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

Binary file not shown.

BIN
server/prisma/dianzhan.db Normal file

Binary file not shown.

View 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");

View File

@@ -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
);

View File

@@ -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");

View File

@@ -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;

View File

@@ -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
);

View 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"

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../src/schemas/user.ts"],"names":[],"mappings":"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"}

85
shared/dist/types/base.d.ts vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,6 @@
/**
* Post 类型定义
* 基于 Prisma 生成的类型
*/
export {};
//# sourceMappingURL=post.js.map

1
shared/dist/types/post.js.map vendored Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,6 @@
/**
* 用户相关类型定义
* User-related type definitions
*/
export {};
//# sourceMappingURL=user.js.map

1
shared/dist/types/user.js.map vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"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"}

48
shared/package.json Normal file
View 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
View 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'

View 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
View 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
View 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'
}[]
}

View 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
View 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
View 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
}

View File

@@ -0,0 +1,6 @@
/**
* Utils 导出索引
* Utils export index
*/
export * from './validation.js'

View 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
View 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
View 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
View 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
View 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 导入这些类型