@deepstorm/cli 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +508 -24
- package/dist/registry.json +168 -0
- package/dist/skills/reef-commit/SKILL.md +27 -100
- package/dist/skills/reef-commit/scripts/branch-check.mjs +109 -0
- package/dist/skills/reef-commit/scripts/check-openspec-status.mjs +92 -0
- package/dist/skills/reef-commit/scripts/collect-git-context.mjs +186 -0
- package/dist/skills/reef-commit/scripts/run-tests.sh +91 -0
- package/dist/skills/reef-commit/scripts/stash-and-switch.sh +44 -0
- package/dist/skills/reef-gen-backend/variants/nodejs/steps.md +53 -0
- package/dist/skills/reef-harden/SKILL.md +11 -19
- package/dist/skills/reef-harden/scripts/find-change-dir.mjs +157 -0
- package/dist/skills/reef-pr/SKILL.md +7 -19
- package/dist/skills/reef-pr/scripts/create-pr.mjs +217 -0
- package/dist/skills/reef-style-backend/fragments/nodejs/eslint-config.json +30 -0
- package/dist/skills/reef-style-backend/fragments/nodejs/nestjs-structure.md +58 -0
- package/dist/skills/reef-style-backend/fragments/nodejs/prettier-config.json +19 -0
- package/dist/skills/reef-style-backend/variants/nodejs/examples/module-example.md +166 -0
- package/dist/skills/reef-style-backend/variants/nodejs/examples/prisma-example.md +115 -0
- package/dist/skills/reef-style-backend/variants/nodejs/quick-reference.md +116 -0
- package/dist/skills/sweep-init/SKILL.md +12 -125
- package/dist/skills/sweep-init/scripts/init-project.mjs +166 -0
- package/dist/skills/sweep-run/SKILL.md +11 -35
- package/dist/skills/sweep-run/scripts/env-manager.mjs +71 -2
- package/dist/skills/sweep-run/scripts/generate-report.mjs +116 -0
- package/package.json +2 -2
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* create-pr.mjs — PR 上下文收集与创建
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* node create-pr.mjs --collect # 收集 PR 上下文
|
|
8
|
+
* node create-pr.mjs --create <args> # 创建 PR
|
|
9
|
+
* node create-pr.mjs --help
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
// ── Pure functions ────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export function checkUncommitted(statusShort) {
|
|
19
|
+
return statusShort.trim().length > 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildPrBody(ctx) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
|
|
25
|
+
lines.push('## 概要');
|
|
26
|
+
if (ctx.proposalTitle) {
|
|
27
|
+
lines.push(ctx.proposalTitle);
|
|
28
|
+
} else if (ctx.commitLog && ctx.commitLog.length > 0) {
|
|
29
|
+
lines.push(ctx.commitLog[0].message);
|
|
30
|
+
} else {
|
|
31
|
+
lines.push(ctx.branch);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
lines.push('');
|
|
35
|
+
lines.push('## 关联');
|
|
36
|
+
lines.push(`Branch: ${ctx.branch}`);
|
|
37
|
+
|
|
38
|
+
if (ctx.diffStat) {
|
|
39
|
+
const s = ctx.diffStat;
|
|
40
|
+
lines.push(`Changes: ${s.files} 文件, +${s.insertions}/-${s.deletions}`);
|
|
41
|
+
lines.push('');
|
|
42
|
+
lines.push('## 变更清单');
|
|
43
|
+
s.changedFiles.forEach(f => lines.push(`- \`${f}\``));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (ctx.commitLog && ctx.commitLog.length > 0) {
|
|
47
|
+
lines.push('');
|
|
48
|
+
lines.push('## Commits');
|
|
49
|
+
ctx.commitLog.forEach(c => lines.push(`- ${c.hash} ${c.message}`));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildGhCreateArgs(opts) {
|
|
56
|
+
const args = ['gh pr create'];
|
|
57
|
+
args.push(`--title "${opts.title}"`);
|
|
58
|
+
args.push(`--body "${opts.body}"`);
|
|
59
|
+
if (opts.reviewer) args.push(`--reviewer ${opts.reviewer}`);
|
|
60
|
+
if (opts.label) args.push(`--label ${opts.label}`);
|
|
61
|
+
if (opts.draft) args.push('--draft');
|
|
62
|
+
return args.join(' ');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Git helpers ────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function exec(command) {
|
|
68
|
+
try {
|
|
69
|
+
return execSync(command, { encoding: 'utf8', timeout: 10000 }).trim();
|
|
70
|
+
} catch {
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function collectContext() {
|
|
76
|
+
const branch = exec('git branch --show-current');
|
|
77
|
+
const forkPoint = exec(
|
|
78
|
+
'git merge-base main HEAD 2>/dev/null || echo ""'
|
|
79
|
+
);
|
|
80
|
+
const diffStat = exec(
|
|
81
|
+
`git diff "${forkPoint || 'main'}..HEAD" --stat`
|
|
82
|
+
);
|
|
83
|
+
const commitLog = forkPoint
|
|
84
|
+
? exec(`git log "${forkPoint}"..HEAD --oneline`)
|
|
85
|
+
: '';
|
|
86
|
+
const statusShort = exec('git status --short');
|
|
87
|
+
|
|
88
|
+
// 查找关联 proposal
|
|
89
|
+
let proposalTitle = '';
|
|
90
|
+
const root = exec(
|
|
91
|
+
'git rev-parse --show-toplevel 2>/dev/null || echo ""'
|
|
92
|
+
);
|
|
93
|
+
if (root && branch) {
|
|
94
|
+
const p = join(root, 'openspec', 'changes', branch, 'proposal.md');
|
|
95
|
+
if (existsSync(p)) {
|
|
96
|
+
const firstLine = readFileSync(p, 'utf8')
|
|
97
|
+
.split('\n')[0] || '';
|
|
98
|
+
proposalTitle = firstLine.replace(/^#+\s*/, '').trim();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
branch,
|
|
104
|
+
hasUncommitted: checkUncommitted(statusShort),
|
|
105
|
+
proposalTitle,
|
|
106
|
+
commitLog: commitLog
|
|
107
|
+
? commitLog.split('\n').filter(Boolean).map(l => {
|
|
108
|
+
const m = l.match(/^(\S+)\s+(.*)/);
|
|
109
|
+
return m ? { hash: m[1], message: m[2] } : null;
|
|
110
|
+
}).filter(Boolean)
|
|
111
|
+
: [],
|
|
112
|
+
diffStat: diffStat ? parseDiffStat(diffStat) : null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseDiffStat(output) {
|
|
117
|
+
const lines = output.trim().split('\n').filter(Boolean);
|
|
118
|
+
const changedFiles = [];
|
|
119
|
+
let files = 0, insertions = 0, deletions = 0;
|
|
120
|
+
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
const sm = line.match(/(\d+)\s+files?\s+changed/);
|
|
123
|
+
if (sm) {
|
|
124
|
+
files = parseInt(sm[1], 10);
|
|
125
|
+
const im = line.match(/(\d+)\s+insertions?\(\+\)/);
|
|
126
|
+
if (im) insertions = parseInt(im[1], 10);
|
|
127
|
+
const dm = line.match(/(\d+)\s+deletions?\(-\)/);
|
|
128
|
+
if (dm) deletions = parseInt(dm[1], 10);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const fm = line.match(/^\s*(.+?)\s+\|/);
|
|
132
|
+
if (fm) changedFiles.push(fm[1].trim());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { files, insertions, deletions, changedFiles };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function checkExistingPR() {
|
|
139
|
+
const out = exec('gh pr view --json url 2>/dev/null || true');
|
|
140
|
+
return out ? { exists: true, url: out } : { exists: false };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── CLI entry ─────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
if (import.meta.filename === process.argv[1]) {
|
|
146
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
147
|
+
console.log(`
|
|
148
|
+
create-pr.mjs — PR 上下文收集与创建
|
|
149
|
+
|
|
150
|
+
用法:
|
|
151
|
+
node create-pr.mjs --collect 收集 PR 上下文(JSON)
|
|
152
|
+
node create-pr.mjs --create 创建 PR
|
|
153
|
+
node create-pr.mjs --help 显示帮助
|
|
154
|
+
|
|
155
|
+
--create 选项:
|
|
156
|
+
--title <text> PR 标题
|
|
157
|
+
--body <text> PR 正文
|
|
158
|
+
--reviewer <user> Reviewer
|
|
159
|
+
--label <label> 标签
|
|
160
|
+
--draft 创建 Draft PR
|
|
161
|
+
`);
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (process.argv.includes('--collect')) {
|
|
166
|
+
const ctx = collectContext();
|
|
167
|
+
process.stdout.write(JSON.stringify(ctx, null, 2) + '\n');
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (process.argv.includes('--create')) {
|
|
172
|
+
const titleArg = process.argv.indexOf('--title');
|
|
173
|
+
const bodyArg = process.argv.indexOf('--body');
|
|
174
|
+
const reviewerArg = process.argv.indexOf('--reviewer');
|
|
175
|
+
const labelArg = process.argv.indexOf('--label');
|
|
176
|
+
const isDraft = process.argv.includes('--draft');
|
|
177
|
+
|
|
178
|
+
if (titleArg < 0 || bodyArg < 0) {
|
|
179
|
+
console.error('--title 和 --body 必填');
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const existing = checkExistingPR();
|
|
184
|
+
if (existing.exists) {
|
|
185
|
+
process.stdout.write(JSON.stringify(existing, null, 2) + '\n');
|
|
186
|
+
process.exit(0);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const branch = exec('git branch --show-current');
|
|
190
|
+
const pushOut = exec(`git push -u origin ${branch} 2>&1`);
|
|
191
|
+
if (!pushOut) {
|
|
192
|
+
console.error('git push 失败');
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const cmd = buildGhCreateArgs({
|
|
197
|
+
title: process.argv[titleArg + 1],
|
|
198
|
+
body: process.argv[bodyArg + 1],
|
|
199
|
+
branch,
|
|
200
|
+
reviewer: reviewerArg >= 0
|
|
201
|
+
? process.argv[reviewerArg + 1] : null,
|
|
202
|
+
label: labelArg >= 0 ? process.argv[labelArg + 1] : null,
|
|
203
|
+
draft: isDraft,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const out = exec(cmd);
|
|
207
|
+
process.stdout.write(JSON.stringify({
|
|
208
|
+
created: true,
|
|
209
|
+
url: out,
|
|
210
|
+
}, null, 2) + '\n');
|
|
211
|
+
process.exit(0);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 默认:--collect
|
|
215
|
+
const ctx = collectContext();
|
|
216
|
+
process.stdout.write(JSON.stringify(ctx, null, 2) + '\n');
|
|
217
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": true,
|
|
3
|
+
"env": {
|
|
4
|
+
"node": true,
|
|
5
|
+
"jest": true
|
|
6
|
+
},
|
|
7
|
+
"parser": "@typescript-eslint/parser",
|
|
8
|
+
"parserOptions": {
|
|
9
|
+
"project": "tsconfig.json",
|
|
10
|
+
"sourceType": "module"
|
|
11
|
+
},
|
|
12
|
+
"plugins": ["@typescript-eslint/eslint-plugin"],
|
|
13
|
+
"extends": [
|
|
14
|
+
"plugin:@typescript-eslint/recommended",
|
|
15
|
+
"plugin:prettier/recommended"
|
|
16
|
+
],
|
|
17
|
+
"rules": {
|
|
18
|
+
"@typescript-eslint/no-explicit-any": "error",
|
|
19
|
+
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
|
20
|
+
"@typescript-eslint/explicit-function-return-type": "off",
|
|
21
|
+
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
22
|
+
"no-console": "warn",
|
|
23
|
+
"prefer-const": "error",
|
|
24
|
+
"no-var": "error",
|
|
25
|
+
"eqeqeq": ["error", "always"],
|
|
26
|
+
"curly": ["error", "all"],
|
|
27
|
+
"no-throw-literal": "error",
|
|
28
|
+
"prefer-promise-reject-errors": "error"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# NestJS 项目结构规范
|
|
2
|
+
|
|
3
|
+
## 顶层目录
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
src/
|
|
7
|
+
├── main.ts # 入口(Bootstrap)
|
|
8
|
+
├── app.module.ts # 根模块
|
|
9
|
+
├── app.controller.ts # 健康检查
|
|
10
|
+
└── app.service.ts # 全局服务
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 模块目录规范
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
src/modules/{module-name}/
|
|
17
|
+
├── {module-name}.module.ts # Module 定义
|
|
18
|
+
├── {module-name}.controller.ts # REST Controller
|
|
19
|
+
├── {module-name}.service.ts # 业务逻辑
|
|
20
|
+
├── dto/
|
|
21
|
+
│ ├── create-{entity}.dto.ts # 创建 DTO
|
|
22
|
+
│ └── update-{entity}.dto.ts # 更新 DTO(PartialType)
|
|
23
|
+
└── entities/
|
|
24
|
+
└── {entity}.entity.ts # Prisma 封装类型(可选)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 公共目录
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
src/
|
|
31
|
+
├── prisma/
|
|
32
|
+
│ ├── prisma.module.ts
|
|
33
|
+
│ └── prisma.service.ts
|
|
34
|
+
├── common/
|
|
35
|
+
│ ├── filters/
|
|
36
|
+
│ │ └── http-exception.filter.ts
|
|
37
|
+
│ ├── pipes/
|
|
38
|
+
│ │ └── validation.pipe.ts
|
|
39
|
+
│ ├── interceptors/
|
|
40
|
+
│ │ ├── transform.interceptor.ts
|
|
41
|
+
│ │ └── logging.interceptor.ts
|
|
42
|
+
│ └── guards/
|
|
43
|
+
│ └── auth.guard.ts
|
|
44
|
+
└── config/
|
|
45
|
+
└── configuration.ts
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 命名规范
|
|
49
|
+
|
|
50
|
+
| 类型 | 命名规则 | 示例 |
|
|
51
|
+
|------|---------|------|
|
|
52
|
+
| Module 类 | PascalCase + Module | `UsersModule` |
|
|
53
|
+
| Controller 类 | PascalCase + Controller | `UsersController` |
|
|
54
|
+
| Service 类 | PascalCase + Service | `UsersService` |
|
|
55
|
+
| DTO 类 | PascalCase + Dto | `CreateUserDto` |
|
|
56
|
+
| Module 目录 | kebab-case | `src/modules/user-roles/` |
|
|
57
|
+
| DTO 文件 | kebab-case | `create-user-role.dto.ts` |
|
|
58
|
+
| 配置键 | UPPER_SNAKE_CASE | `DATABASE_URL` |
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"semi": true,
|
|
3
|
+
"singleQuote": true,
|
|
4
|
+
"tabWidth": 2,
|
|
5
|
+
"trailingComma": "all",
|
|
6
|
+
"printWidth": 100,
|
|
7
|
+
"bracketSpacing": true,
|
|
8
|
+
"arrowParens": "always",
|
|
9
|
+
"endOfLine": "lf",
|
|
10
|
+
"quoteProps": "as-needed",
|
|
11
|
+
"overrides": [
|
|
12
|
+
{
|
|
13
|
+
"files": "*.prisma",
|
|
14
|
+
"options": {
|
|
15
|
+
"tabWidth": 2
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# NestJS Module 示例 — Users 模块
|
|
2
|
+
|
|
3
|
+
```typescript
|
|
4
|
+
// users.module.ts
|
|
5
|
+
import { Module } from '@nestjs/common';
|
|
6
|
+
import { UsersController } from './users.controller';
|
|
7
|
+
import { UsersService } from './users.service';
|
|
8
|
+
import { PrismaModule } from '../prisma/prisma.module';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 用户管理模块
|
|
12
|
+
* 提供用户 CRUD 操作和权限管理
|
|
13
|
+
*/
|
|
14
|
+
@Module({
|
|
15
|
+
imports: [PrismaModule],
|
|
16
|
+
controllers: [UsersController],
|
|
17
|
+
providers: [UsersService],
|
|
18
|
+
exports: [UsersService],
|
|
19
|
+
})
|
|
20
|
+
export class UsersModule {}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// users.controller.ts
|
|
25
|
+
import {
|
|
26
|
+
Controller, Get, Post, Body, Param, Put, Delete,
|
|
27
|
+
ParseIntPipe,
|
|
28
|
+
} from '@nestjs/common';
|
|
29
|
+
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
|
30
|
+
import { UsersService } from './users.service';
|
|
31
|
+
import { CreateUserDto } from './dto/create-user.dto';
|
|
32
|
+
import { UpdateUserDto } from './dto/update-user.dto';
|
|
33
|
+
|
|
34
|
+
@ApiTags('Users')
|
|
35
|
+
@Controller('users')
|
|
36
|
+
@ApiBearerAuth()
|
|
37
|
+
export class UsersController {
|
|
38
|
+
constructor(private readonly usersService: UsersService) {}
|
|
39
|
+
|
|
40
|
+
@Post()
|
|
41
|
+
@ApiOperation({ summary: '创建用户', description: '创建一个新的系统用户' })
|
|
42
|
+
create(@Body() createUserDto: CreateUserDto) {
|
|
43
|
+
return this.usersService.create(createUserDto);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Get()
|
|
47
|
+
@ApiOperation({ summary: '查询用户列表' })
|
|
48
|
+
findAll() {
|
|
49
|
+
return this.usersService.findAll();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Get(':id')
|
|
53
|
+
@ApiOperation({ summary: '查询单个用户' })
|
|
54
|
+
findOne(@Param('id', ParseIntPipe) id: number) {
|
|
55
|
+
return this.usersService.findOne(id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Put(':id')
|
|
59
|
+
@ApiOperation({ summary: '更新用户信息' })
|
|
60
|
+
update(
|
|
61
|
+
@Param('id', ParseIntPipe) id: number,
|
|
62
|
+
@Body() updateUserDto: UpdateUserDto,
|
|
63
|
+
) {
|
|
64
|
+
return this.usersService.update(id, updateUserDto);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Delete(':id')
|
|
68
|
+
@ApiOperation({ summary: '删除用户' })
|
|
69
|
+
remove(@Param('id', ParseIntPipe) id: number) {
|
|
70
|
+
return this.usersService.remove(id);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// users.service.ts
|
|
77
|
+
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
78
|
+
import { PrismaService } from '../prisma/prisma.service';
|
|
79
|
+
import { CreateUserDto } from './dto/create-user.dto';
|
|
80
|
+
import { UpdateUserDto } from './dto/update-user.dto';
|
|
81
|
+
|
|
82
|
+
@Injectable()
|
|
83
|
+
export class UsersService {
|
|
84
|
+
private readonly logger = new Logger(UsersService.name);
|
|
85
|
+
|
|
86
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 创建用户
|
|
90
|
+
* @param createUserDto 用户创建参数
|
|
91
|
+
* @returns 创建的用户记录
|
|
92
|
+
*/
|
|
93
|
+
async create(createUserDto: CreateUserDto) {
|
|
94
|
+
return this.prisma.user.create({ data: createUserDto });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 查询所有用户 */
|
|
98
|
+
async findAll() {
|
|
99
|
+
return this.prisma.user.findMany();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 根据 ID 查询用户
|
|
104
|
+
* @param id 用户 ID
|
|
105
|
+
* @throws NotFoundException 用户不存在
|
|
106
|
+
*/
|
|
107
|
+
async findOne(id: number) {
|
|
108
|
+
const user = await this.prisma.user.findUnique({ where: { id } });
|
|
109
|
+
if (!user) throw new NotFoundException(`User #${id} not found`);
|
|
110
|
+
return user;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 更新用户信息
|
|
115
|
+
* @param id 用户 ID
|
|
116
|
+
* @param updateUserDto 更新参数
|
|
117
|
+
*/
|
|
118
|
+
async update(id: number, updateUserDto: UpdateUserDto) {
|
|
119
|
+
await this.findOne(id);
|
|
120
|
+
return this.prisma.user.update({
|
|
121
|
+
where: { id },
|
|
122
|
+
data: updateUserDto,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 删除用户
|
|
128
|
+
* @param id 用户 ID
|
|
129
|
+
*/
|
|
130
|
+
async remove(id: number) {
|
|
131
|
+
await this.findOne(id);
|
|
132
|
+
return this.prisma.user.delete({ where: { id } });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// dto/create-user.dto.ts
|
|
139
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
140
|
+
import { IsString, IsEmail, IsOptional, IsBoolean } from 'class-validator';
|
|
141
|
+
|
|
142
|
+
/** 创建用户请求 DTO */
|
|
143
|
+
export class CreateUserDto {
|
|
144
|
+
@ApiProperty({ description: '用户名', example: 'john_doe' })
|
|
145
|
+
@IsString()
|
|
146
|
+
name!: string;
|
|
147
|
+
|
|
148
|
+
@ApiProperty({ description: '邮箱地址', example: 'john@example.com' })
|
|
149
|
+
@IsEmail()
|
|
150
|
+
email!: string;
|
|
151
|
+
|
|
152
|
+
@ApiProperty({ description: '是否激活', required: false, default: true })
|
|
153
|
+
@IsOptional()
|
|
154
|
+
@IsBoolean()
|
|
155
|
+
active?: boolean;
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// dto/update-user.dto.ts
|
|
161
|
+
import { PartialType } from '@nestjs/swagger';
|
|
162
|
+
import { CreateUserDto } from './create-user.dto';
|
|
163
|
+
|
|
164
|
+
/** 更新用户请求 DTO — 所有字段可选 */
|
|
165
|
+
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
|
166
|
+
```
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Prisma Service 与 Schema 示例
|
|
2
|
+
|
|
3
|
+
## Prisma Schema
|
|
4
|
+
|
|
5
|
+
```prisma
|
|
6
|
+
// prisma/schema.prisma
|
|
7
|
+
|
|
8
|
+
generator client {
|
|
9
|
+
provider = "prisma-client-js"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
datasource db {
|
|
13
|
+
provider = "postgresql"
|
|
14
|
+
url = env("DATABASE_URL")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// 系统用户
|
|
18
|
+
model User {
|
|
19
|
+
id Int @id @default(autoincrement())
|
|
20
|
+
name String
|
|
21
|
+
email String @unique
|
|
22
|
+
active Boolean @default(true)
|
|
23
|
+
posts Post[]
|
|
24
|
+
createdAt DateTime @default(now())
|
|
25
|
+
updatedAt DateTime @updatedAt
|
|
26
|
+
|
|
27
|
+
@@map("users")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// 文章
|
|
31
|
+
model Post {
|
|
32
|
+
id Int @id @default(autoincrement())
|
|
33
|
+
title String
|
|
34
|
+
content String
|
|
35
|
+
published Boolean @default(false)
|
|
36
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
37
|
+
authorId Int
|
|
38
|
+
createdAt DateTime @default(now())
|
|
39
|
+
updatedAt DateTime @updatedAt
|
|
40
|
+
|
|
41
|
+
@@map("posts")
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Prisma Module & Service
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// prisma/prisma.module.ts
|
|
49
|
+
import { Global, Module } from '@nestjs/common';
|
|
50
|
+
import { PrismaService } from './prisma.service';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Prisma 全局模块
|
|
54
|
+
* 作为 Global 模块,所有其他模块无需 imports 即可注入 PrismaService
|
|
55
|
+
*/
|
|
56
|
+
@Global()
|
|
57
|
+
@Module({
|
|
58
|
+
providers: [PrismaService],
|
|
59
|
+
exports: [PrismaService],
|
|
60
|
+
})
|
|
61
|
+
export class PrismaModule {}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// prisma/prisma.service.ts
|
|
66
|
+
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
|
67
|
+
import { PrismaClient } from '@prisma/client';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Prisma 数据库服务
|
|
71
|
+
* 封装 PrismaClient,提供数据库连接生命周期管理
|
|
72
|
+
*/
|
|
73
|
+
@Injectable()
|
|
74
|
+
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
|
75
|
+
private readonly logger = new Logger(PrismaService.name);
|
|
76
|
+
|
|
77
|
+
async onModuleInit() {
|
|
78
|
+
await this.$connect();
|
|
79
|
+
this.logger.log('Database connected');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async onModuleDestroy() {
|
|
83
|
+
await this.$disconnect();
|
|
84
|
+
this.logger.log('Database disconnected');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Prisma 查询最佳实践
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// 使用事务:多个原子操作
|
|
93
|
+
const [user, post] = await this.prisma.$transaction([
|
|
94
|
+
this.prisma.user.create({ data: { name: 'Alice', email: 'alice@test.com' } }),
|
|
95
|
+
this.prisma.post.create({ data: { title: 'Hello', content: 'World', authorId: 1 } }),
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
// 使用 select 限制返回字段,避免 N+1
|
|
99
|
+
const users = await this.prisma.user.findMany({
|
|
100
|
+
select: {
|
|
101
|
+
id: true,
|
|
102
|
+
name: true,
|
|
103
|
+
email: true,
|
|
104
|
+
posts: {
|
|
105
|
+
select: { title: true, published: true },
|
|
106
|
+
where: { published: true },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 使用 findFirstOrThrow 简化判空
|
|
112
|
+
const user = await this.prisma.user.findFirstOrThrow({
|
|
113
|
+
where: { email: 'alice@test.com' },
|
|
114
|
+
});
|
|
115
|
+
```
|