@relipa/ai-flow-kit 0.0.6-beta.0 → 0.0.7-beta.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/README.md +33 -13
- package/bin/aiflow.js +58 -2
- package/custom/mcp-presets/gitnexus.json +8 -0
- package/custom/rules/java/spring-boot-examples.md +329 -329
- package/custom/skills/generate-spec/SKILL.md +7 -0
- package/custom/skills/impact-analysis/SKILL.md +10 -0
- package/custom/skills/read-study-requirement/SKILL.md +11 -0
- package/custom/skills/review-plan/SKILL.md +15 -0
- package/custom/templates/spring-boot.md +224 -224
- package/docs/common/AIFLOW.md +12 -6
- package/docs/common/CHANGELOG.md +49 -5
- package/docs/common/QUICK_START.md +13 -11
- package/docs/common/cli-reference.md +23 -0
- package/package.json +2 -7
- package/scripts/checkpoint.js +46 -0
- package/scripts/doctor.js +192 -89
- package/scripts/gitnexus-worker.js +94 -0
- package/scripts/guide.js +42 -51
- package/scripts/hooks/session-start.js +274 -244
- package/scripts/hooks/session-stop.js +55 -0
- package/scripts/init.js +293 -18
- package/scripts/prompt.js +2 -2
- package/scripts/remove.js +54 -0
- package/scripts/task.js +446 -384
- package/scripts/update.js +14 -4
- package/scripts/use.js +41 -0
- package/upstream/.claude-plugin/marketplace.json +4 -4
- package/upstream/.claude-plugin/plugin.json +2 -4
- package/upstream/.cursor-plugin/plugin.json +2 -4
- package/upstream/docs/plans/2025-11-22-opencode-support-design.md +1 -1
- package/upstream/docs/plans/2025-11-28-skills-improvements-from-user-feedback.md +2 -2
- package/upstream/docs/testing.md +2 -2
- package/upstream/skills/subagent-driven-development/SKILL.md +5 -6
- package/upstream/skills/subagent-driven-development/implementer-prompt.md +2 -3
- package/upstream/skills/systematic-debugging/CREATION-LOG.md +1 -1
- package/upstream/skills/systematic-debugging/root-cause-tracing.md +1 -1
- package/upstream/skills/tdd-lean/SKILL.md +127 -0
- package/upstream/skills/using-git-worktrees/SKILL.md +4 -5
- package/upstream/skills/writing-plans/SKILL.md +3 -9
- package/upstream/tests/brainstorm-server/package-lock.json +36 -0
|
@@ -1,329 +1,329 @@
|
|
|
1
|
-
# Spring Boot Code Examples
|
|
2
|
-
|
|
3
|
-
Reference examples for each layer. Read the relevant section when generating code for that layer.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Controller
|
|
8
|
-
|
|
9
|
-
```java
|
|
10
|
-
// ✅ Good
|
|
11
|
-
\@PostMapping
|
|
12
|
-
public ResponseEntity<UserResponse> createUser(
|
|
13
|
-
\@Valid \@RequestBody CreateUserRequest request) {
|
|
14
|
-
UserResponse response = userService.create(request);
|
|
15
|
-
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// ❌ Bad — business logic in controller
|
|
19
|
-
\@PostMapping
|
|
20
|
-
public ResponseEntity<User> createUser(\@RequestBody User user) {
|
|
21
|
-
if (userRepository.existsByEmail(user.getEmail())) {
|
|
22
|
-
throw new RuntimeException("Email exists");
|
|
23
|
-
}
|
|
24
|
-
return ResponseEntity.ok(userRepository.save(user));
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
## Service
|
|
31
|
-
|
|
32
|
-
```java
|
|
33
|
-
// Interface
|
|
34
|
-
public interface UserService {
|
|
35
|
-
UserResponse create(CreateUserRequest request);
|
|
36
|
-
UserResponse findById(Long id);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Implementation
|
|
40
|
-
\@Service
|
|
41
|
-
\@RequiredArgsConstructor
|
|
42
|
-
public class UserServiceImpl implements UserService {
|
|
43
|
-
|
|
44
|
-
private final UserRepository userRepository;
|
|
45
|
-
private final UserMapper userMapper;
|
|
46
|
-
|
|
47
|
-
\@Override
|
|
48
|
-
\@Transactional
|
|
49
|
-
public UserResponse create(CreateUserRequest request) {
|
|
50
|
-
if (userRepository.existsByEmail(request.getEmail())) {
|
|
51
|
-
throw new DuplicateEmailException(request.getEmail());
|
|
52
|
-
}
|
|
53
|
-
User user = userMapper.toEntity(request);
|
|
54
|
-
User saved = userRepository.save(user);
|
|
55
|
-
return userMapper.toResponse(saved);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
\@Override
|
|
59
|
-
\@Transactional(readOnly = true)
|
|
60
|
-
public UserResponse findById(Long id) {
|
|
61
|
-
log.info("Finding user by id: {}", id);
|
|
62
|
-
return userRepository.findById(id)
|
|
63
|
-
.map(userMapper::toResponse)
|
|
64
|
-
.orElseThrow(() -> {
|
|
65
|
-
log.warn("User not found: id={}", id);
|
|
66
|
-
return new ResourceNotFoundException("User", id);
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
\@Transactional(readOnly = true)
|
|
71
|
-
public Page<UserResponse> findAll(Pageable pageable) {
|
|
72
|
-
return userRepository.findAll(pageable).map(userMapper::toResponse);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
## Repository
|
|
80
|
-
|
|
81
|
-
```java
|
|
82
|
-
// ✅ Good
|
|
83
|
-
public interface UserRepository extends JpaRepository<User, Long> {
|
|
84
|
-
|
|
85
|
-
boolean existsByEmail(String email);
|
|
86
|
-
|
|
87
|
-
Optional<User> findByEmailAndDeletedFalse(String email);
|
|
88
|
-
|
|
89
|
-
\@Query("SELECT u FROM User u JOIN FETCH u.roles WHERE u.id = :id")
|
|
90
|
-
Optional<User> findByIdWithRoles(\@Param("id") Long id);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ❌ Bad — N+1 problem
|
|
94
|
-
List<Order> findAll(); // If Order has \@ManyToOne User, this causes N+1
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## Entity
|
|
100
|
-
|
|
101
|
-
```java
|
|
102
|
-
\@Entity
|
|
103
|
-
\@Table(name = "users")
|
|
104
|
-
\@Getter
|
|
105
|
-
\@Setter
|
|
106
|
-
\@NoArgsConstructor
|
|
107
|
-
\@AllArgsConstructor
|
|
108
|
-
\@Builder
|
|
109
|
-
public class User extends BaseEntity {
|
|
110
|
-
|
|
111
|
-
\@Id
|
|
112
|
-
\@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
113
|
-
private Long id;
|
|
114
|
-
|
|
115
|
-
\@Column(name = "email", nullable = false, unique = true, length = 255)
|
|
116
|
-
private String email;
|
|
117
|
-
|
|
118
|
-
\@Column(name = "full_name", nullable = false, length = 100)
|
|
119
|
-
private String fullName;
|
|
120
|
-
|
|
121
|
-
\@Column(name = "deleted", nullable = false)
|
|
122
|
-
\@Builder.Default
|
|
123
|
-
private boolean deleted = false;
|
|
124
|
-
|
|
125
|
-
\@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
|
126
|
-
private List<Order> orders = new ArrayList<>();
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
## DTO
|
|
133
|
-
|
|
134
|
-
```java
|
|
135
|
-
// Request DTO
|
|
136
|
-
\@Getter
|
|
137
|
-
\@NoArgsConstructor
|
|
138
|
-
\@AllArgsConstructor
|
|
139
|
-
\@Builder
|
|
140
|
-
public class CreateUserRequest {
|
|
141
|
-
|
|
142
|
-
\@NotBlank(message = "Email is required")
|
|
143
|
-
\@Email(message = "Invalid email format")
|
|
144
|
-
private String email;
|
|
145
|
-
|
|
146
|
-
\@NotBlank(message = "Full name is required")
|
|
147
|
-
\@Size(min = 2, max = 100, message = "Full name must be 2-100 characters")
|
|
148
|
-
private String fullName;
|
|
149
|
-
|
|
150
|
-
\@NotBlank(message = "Password is required")
|
|
151
|
-
\@Size(min = 8, message = "Password must be at least 8 characters")
|
|
152
|
-
private String password;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Response DTO
|
|
156
|
-
\@Getter
|
|
157
|
-
\@NoArgsConstructor
|
|
158
|
-
\@AllArgsConstructor
|
|
159
|
-
\@Builder
|
|
160
|
-
public class UserResponse {
|
|
161
|
-
private Long id;
|
|
162
|
-
private String email;
|
|
163
|
-
private String fullName;
|
|
164
|
-
private LocalDateTime createdAt;
|
|
165
|
-
}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
---
|
|
169
|
-
|
|
170
|
-
## MapStruct Mapper
|
|
171
|
-
|
|
172
|
-
```java
|
|
173
|
-
\@Mapper(componentModel = "spring")
|
|
174
|
-
public interface UserMapper {
|
|
175
|
-
|
|
176
|
-
\@Mapping(target = "createdAt", source = "createdAt")
|
|
177
|
-
UserResponse toResponse(User user);
|
|
178
|
-
|
|
179
|
-
\@Mapping(target = "id", ignore = true)
|
|
180
|
-
\@Mapping(target = "deleted", ignore = true)
|
|
181
|
-
\@Mapping(target = "createdAt", ignore = true)
|
|
182
|
-
\@Mapping(target = "updatedAt", ignore = true)
|
|
183
|
-
User toEntity(CreateUserRequest request);
|
|
184
|
-
|
|
185
|
-
List<UserResponse> toResponseList(List<User> users);
|
|
186
|
-
}
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
---
|
|
190
|
-
|
|
191
|
-
## Exception Handling
|
|
192
|
-
|
|
193
|
-
```java
|
|
194
|
-
// Custom exception
|
|
195
|
-
public class ResourceNotFoundException extends RuntimeException {
|
|
196
|
-
public ResourceNotFoundException(String resource, Long id) {
|
|
197
|
-
super(String.format("%s with id %d not found", resource, id));
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Global handler
|
|
202
|
-
\@RestControllerAdvice
|
|
203
|
-
\@Slf4j
|
|
204
|
-
public class GlobalExceptionHandler {
|
|
205
|
-
|
|
206
|
-
\@ExceptionHandler(ResourceNotFoundException.class)
|
|
207
|
-
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
|
|
208
|
-
log.warn("Resource not found: {}", ex.getMessage());
|
|
209
|
-
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
|
210
|
-
.body(ErrorResponse.of("NOT_FOUND", ex.getMessage()));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
\@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
214
|
-
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
|
|
215
|
-
List<String> errors = ex.getBindingResult()
|
|
216
|
-
.getFieldErrors()
|
|
217
|
-
.stream()
|
|
218
|
-
.map(e -> e.getField() + ": " + e.getDefaultMessage())
|
|
219
|
-
.toList();
|
|
220
|
-
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
|
221
|
-
.body(ErrorResponse.of("VALIDATION_FAILED", errors.toString()));
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
\@ExceptionHandler(Exception.class)
|
|
225
|
-
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
|
|
226
|
-
log.error("Unexpected error", ex);
|
|
227
|
-
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
228
|
-
.body(ErrorResponse.of("INTERNAL_ERROR", "An unexpected error occurred"));
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
---
|
|
234
|
-
|
|
235
|
-
## Unit Tests (Service layer)
|
|
236
|
-
|
|
237
|
-
```java
|
|
238
|
-
\@ExtendWith(MockitoExtension.class)
|
|
239
|
-
class UserServiceImplTest {
|
|
240
|
-
|
|
241
|
-
\@Mock
|
|
242
|
-
private UserRepository userRepository;
|
|
243
|
-
|
|
244
|
-
\@Mock
|
|
245
|
-
private UserMapper userMapper;
|
|
246
|
-
|
|
247
|
-
\@InjectMocks
|
|
248
|
-
private UserServiceImpl userService;
|
|
249
|
-
|
|
250
|
-
\@Test
|
|
251
|
-
void create_shouldReturnUserResponse_whenEmailIsUnique() {
|
|
252
|
-
CreateUserRequest request = CreateUserRequest.builder()
|
|
253
|
-
.email("test\@example.com")
|
|
254
|
-
.fullName("Test User")
|
|
255
|
-
.build();
|
|
256
|
-
User user = User.builder().id(1L).email("test\@example.com").build();
|
|
257
|
-
UserResponse expected = UserResponse.builder().id(1L).build();
|
|
258
|
-
|
|
259
|
-
given(userRepository.existsByEmail(request.getEmail())).willReturn(false);
|
|
260
|
-
given(userMapper.toEntity(request)).willReturn(user);
|
|
261
|
-
given(userRepository.save(user)).willReturn(user);
|
|
262
|
-
given(userMapper.toResponse(user)).willReturn(expected);
|
|
263
|
-
|
|
264
|
-
UserResponse result = userService.create(request);
|
|
265
|
-
|
|
266
|
-
assertThat(result).isEqualTo(expected);
|
|
267
|
-
verify(userRepository).save(user);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
\@Test
|
|
271
|
-
void create_shouldThrowDuplicateEmailException_whenEmailAlreadyExists() {
|
|
272
|
-
CreateUserRequest request = CreateUserRequest.builder()
|
|
273
|
-
.email("existing\@example.com").build();
|
|
274
|
-
given(userRepository.existsByEmail(request.getEmail())).willReturn(true);
|
|
275
|
-
|
|
276
|
-
assertThatThrownBy(() -> userService.create(request))
|
|
277
|
-
.isInstanceOf(DuplicateEmailException.class);
|
|
278
|
-
verify(userRepository, never()).save(any());
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
---
|
|
284
|
-
|
|
285
|
-
## Integration Tests (Controller layer)
|
|
286
|
-
|
|
287
|
-
```java
|
|
288
|
-
\@SpringBootTest
|
|
289
|
-
\@AutoConfigureMockMvc
|
|
290
|
-
class UserControllerIntegrationTest {
|
|
291
|
-
|
|
292
|
-
\@Autowired
|
|
293
|
-
private MockMvc mockMvc;
|
|
294
|
-
|
|
295
|
-
\@Autowired
|
|
296
|
-
private ObjectMapper objectMapper;
|
|
297
|
-
|
|
298
|
-
\@Test
|
|
299
|
-
void createUser_shouldReturn201_whenRequestIsValid() throws Exception {
|
|
300
|
-
CreateUserRequest request = CreateUserRequest.builder()
|
|
301
|
-
.email("test\@example.com")
|
|
302
|
-
.fullName("Test User")
|
|
303
|
-
.password("password123")
|
|
304
|
-
.build();
|
|
305
|
-
|
|
306
|
-
mockMvc.perform(post("/api/v1/users")
|
|
307
|
-
.contentType(MediaType.APPLICATION_JSON)
|
|
308
|
-
.content(objectMapper.writeValueAsString(request)))
|
|
309
|
-
.andExpect(status().isCreated())
|
|
310
|
-
.andExpect(jsonPath("$.email").value("test\@example.com"))
|
|
311
|
-
.andExpect(jsonPath("$.id").isNumber());
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
\@Test
|
|
315
|
-
void createUser_shouldReturn400_whenEmailIsInvalid() throws Exception {
|
|
316
|
-
CreateUserRequest request = CreateUserRequest.builder()
|
|
317
|
-
.email("not-an-email")
|
|
318
|
-
.fullName("Test")
|
|
319
|
-
.password("password123")
|
|
320
|
-
.build();
|
|
321
|
-
|
|
322
|
-
mockMvc.perform(post("/api/v1/users")
|
|
323
|
-
.contentType(MediaType.APPLICATION_JSON)
|
|
324
|
-
.content(objectMapper.writeValueAsString(request)))
|
|
325
|
-
.andExpect(status().isBadRequest())
|
|
326
|
-
.andExpect(jsonPath("$.code").value("VALIDATION_FAILED"));
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
```
|
|
1
|
+
# Spring Boot Code Examples
|
|
2
|
+
|
|
3
|
+
Reference examples for each layer. Read the relevant section when generating code for that layer.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Controller
|
|
8
|
+
|
|
9
|
+
```java
|
|
10
|
+
// ✅ Good
|
|
11
|
+
\@PostMapping
|
|
12
|
+
public ResponseEntity<UserResponse> createUser(
|
|
13
|
+
\@Valid \@RequestBody CreateUserRequest request) {
|
|
14
|
+
UserResponse response = userService.create(request);
|
|
15
|
+
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ❌ Bad — business logic in controller
|
|
19
|
+
\@PostMapping
|
|
20
|
+
public ResponseEntity<User> createUser(\@RequestBody User user) {
|
|
21
|
+
if (userRepository.existsByEmail(user.getEmail())) {
|
|
22
|
+
throw new RuntimeException("Email exists");
|
|
23
|
+
}
|
|
24
|
+
return ResponseEntity.ok(userRepository.save(user));
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Service
|
|
31
|
+
|
|
32
|
+
```java
|
|
33
|
+
// Interface
|
|
34
|
+
public interface UserService {
|
|
35
|
+
UserResponse create(CreateUserRequest request);
|
|
36
|
+
UserResponse findById(Long id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Implementation
|
|
40
|
+
\@Service
|
|
41
|
+
\@RequiredArgsConstructor
|
|
42
|
+
public class UserServiceImpl implements UserService {
|
|
43
|
+
|
|
44
|
+
private final UserRepository userRepository;
|
|
45
|
+
private final UserMapper userMapper;
|
|
46
|
+
|
|
47
|
+
\@Override
|
|
48
|
+
\@Transactional
|
|
49
|
+
public UserResponse create(CreateUserRequest request) {
|
|
50
|
+
if (userRepository.existsByEmail(request.getEmail())) {
|
|
51
|
+
throw new DuplicateEmailException(request.getEmail());
|
|
52
|
+
}
|
|
53
|
+
User user = userMapper.toEntity(request);
|
|
54
|
+
User saved = userRepository.save(user);
|
|
55
|
+
return userMapper.toResponse(saved);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
\@Override
|
|
59
|
+
\@Transactional(readOnly = true)
|
|
60
|
+
public UserResponse findById(Long id) {
|
|
61
|
+
log.info("Finding user by id: {}", id);
|
|
62
|
+
return userRepository.findById(id)
|
|
63
|
+
.map(userMapper::toResponse)
|
|
64
|
+
.orElseThrow(() -> {
|
|
65
|
+
log.warn("User not found: id={}", id);
|
|
66
|
+
return new ResourceNotFoundException("User", id);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
\@Transactional(readOnly = true)
|
|
71
|
+
public Page<UserResponse> findAll(Pageable pageable) {
|
|
72
|
+
return userRepository.findAll(pageable).map(userMapper::toResponse);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Repository
|
|
80
|
+
|
|
81
|
+
```java
|
|
82
|
+
// ✅ Good
|
|
83
|
+
public interface UserRepository extends JpaRepository<User, Long> {
|
|
84
|
+
|
|
85
|
+
boolean existsByEmail(String email);
|
|
86
|
+
|
|
87
|
+
Optional<User> findByEmailAndDeletedFalse(String email);
|
|
88
|
+
|
|
89
|
+
\@Query("SELECT u FROM User u JOIN FETCH u.roles WHERE u.id = :id")
|
|
90
|
+
Optional<User> findByIdWithRoles(\@Param("id") Long id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ❌ Bad — N+1 problem
|
|
94
|
+
List<Order> findAll(); // If Order has \@ManyToOne User, this causes N+1
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Entity
|
|
100
|
+
|
|
101
|
+
```java
|
|
102
|
+
\@Entity
|
|
103
|
+
\@Table(name = "users")
|
|
104
|
+
\@Getter
|
|
105
|
+
\@Setter
|
|
106
|
+
\@NoArgsConstructor
|
|
107
|
+
\@AllArgsConstructor
|
|
108
|
+
\@Builder
|
|
109
|
+
public class User extends BaseEntity {
|
|
110
|
+
|
|
111
|
+
\@Id
|
|
112
|
+
\@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
113
|
+
private Long id;
|
|
114
|
+
|
|
115
|
+
\@Column(name = "email", nullable = false, unique = true, length = 255)
|
|
116
|
+
private String email;
|
|
117
|
+
|
|
118
|
+
\@Column(name = "full_name", nullable = false, length = 100)
|
|
119
|
+
private String fullName;
|
|
120
|
+
|
|
121
|
+
\@Column(name = "deleted", nullable = false)
|
|
122
|
+
\@Builder.Default
|
|
123
|
+
private boolean deleted = false;
|
|
124
|
+
|
|
125
|
+
\@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
|
126
|
+
private List<Order> orders = new ArrayList<>();
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## DTO
|
|
133
|
+
|
|
134
|
+
```java
|
|
135
|
+
// Request DTO
|
|
136
|
+
\@Getter
|
|
137
|
+
\@NoArgsConstructor
|
|
138
|
+
\@AllArgsConstructor
|
|
139
|
+
\@Builder
|
|
140
|
+
public class CreateUserRequest {
|
|
141
|
+
|
|
142
|
+
\@NotBlank(message = "Email is required")
|
|
143
|
+
\@Email(message = "Invalid email format")
|
|
144
|
+
private String email;
|
|
145
|
+
|
|
146
|
+
\@NotBlank(message = "Full name is required")
|
|
147
|
+
\@Size(min = 2, max = 100, message = "Full name must be 2-100 characters")
|
|
148
|
+
private String fullName;
|
|
149
|
+
|
|
150
|
+
\@NotBlank(message = "Password is required")
|
|
151
|
+
\@Size(min = 8, message = "Password must be at least 8 characters")
|
|
152
|
+
private String password;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Response DTO
|
|
156
|
+
\@Getter
|
|
157
|
+
\@NoArgsConstructor
|
|
158
|
+
\@AllArgsConstructor
|
|
159
|
+
\@Builder
|
|
160
|
+
public class UserResponse {
|
|
161
|
+
private Long id;
|
|
162
|
+
private String email;
|
|
163
|
+
private String fullName;
|
|
164
|
+
private LocalDateTime createdAt;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## MapStruct Mapper
|
|
171
|
+
|
|
172
|
+
```java
|
|
173
|
+
\@Mapper(componentModel = "spring")
|
|
174
|
+
public interface UserMapper {
|
|
175
|
+
|
|
176
|
+
\@Mapping(target = "createdAt", source = "createdAt")
|
|
177
|
+
UserResponse toResponse(User user);
|
|
178
|
+
|
|
179
|
+
\@Mapping(target = "id", ignore = true)
|
|
180
|
+
\@Mapping(target = "deleted", ignore = true)
|
|
181
|
+
\@Mapping(target = "createdAt", ignore = true)
|
|
182
|
+
\@Mapping(target = "updatedAt", ignore = true)
|
|
183
|
+
User toEntity(CreateUserRequest request);
|
|
184
|
+
|
|
185
|
+
List<UserResponse> toResponseList(List<User> users);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Exception Handling
|
|
192
|
+
|
|
193
|
+
```java
|
|
194
|
+
// Custom exception
|
|
195
|
+
public class ResourceNotFoundException extends RuntimeException {
|
|
196
|
+
public ResourceNotFoundException(String resource, Long id) {
|
|
197
|
+
super(String.format("%s with id %d not found", resource, id));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Global handler
|
|
202
|
+
\@RestControllerAdvice
|
|
203
|
+
\@Slf4j
|
|
204
|
+
public class GlobalExceptionHandler {
|
|
205
|
+
|
|
206
|
+
\@ExceptionHandler(ResourceNotFoundException.class)
|
|
207
|
+
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
|
|
208
|
+
log.warn("Resource not found: {}", ex.getMessage());
|
|
209
|
+
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
|
210
|
+
.body(ErrorResponse.of("NOT_FOUND", ex.getMessage()));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
\@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
214
|
+
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
|
|
215
|
+
List<String> errors = ex.getBindingResult()
|
|
216
|
+
.getFieldErrors()
|
|
217
|
+
.stream()
|
|
218
|
+
.map(e -> e.getField() + ": " + e.getDefaultMessage())
|
|
219
|
+
.toList();
|
|
220
|
+
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
|
221
|
+
.body(ErrorResponse.of("VALIDATION_FAILED", errors.toString()));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
\@ExceptionHandler(Exception.class)
|
|
225
|
+
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
|
|
226
|
+
log.error("Unexpected error", ex);
|
|
227
|
+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
228
|
+
.body(ErrorResponse.of("INTERNAL_ERROR", "An unexpected error occurred"));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Unit Tests (Service layer)
|
|
236
|
+
|
|
237
|
+
```java
|
|
238
|
+
\@ExtendWith(MockitoExtension.class)
|
|
239
|
+
class UserServiceImplTest {
|
|
240
|
+
|
|
241
|
+
\@Mock
|
|
242
|
+
private UserRepository userRepository;
|
|
243
|
+
|
|
244
|
+
\@Mock
|
|
245
|
+
private UserMapper userMapper;
|
|
246
|
+
|
|
247
|
+
\@InjectMocks
|
|
248
|
+
private UserServiceImpl userService;
|
|
249
|
+
|
|
250
|
+
\@Test
|
|
251
|
+
void create_shouldReturnUserResponse_whenEmailIsUnique() {
|
|
252
|
+
CreateUserRequest request = CreateUserRequest.builder()
|
|
253
|
+
.email("test\@example.com")
|
|
254
|
+
.fullName("Test User")
|
|
255
|
+
.build();
|
|
256
|
+
User user = User.builder().id(1L).email("test\@example.com").build();
|
|
257
|
+
UserResponse expected = UserResponse.builder().id(1L).build();
|
|
258
|
+
|
|
259
|
+
given(userRepository.existsByEmail(request.getEmail())).willReturn(false);
|
|
260
|
+
given(userMapper.toEntity(request)).willReturn(user);
|
|
261
|
+
given(userRepository.save(user)).willReturn(user);
|
|
262
|
+
given(userMapper.toResponse(user)).willReturn(expected);
|
|
263
|
+
|
|
264
|
+
UserResponse result = userService.create(request);
|
|
265
|
+
|
|
266
|
+
assertThat(result).isEqualTo(expected);
|
|
267
|
+
verify(userRepository).save(user);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
\@Test
|
|
271
|
+
void create_shouldThrowDuplicateEmailException_whenEmailAlreadyExists() {
|
|
272
|
+
CreateUserRequest request = CreateUserRequest.builder()
|
|
273
|
+
.email("existing\@example.com").build();
|
|
274
|
+
given(userRepository.existsByEmail(request.getEmail())).willReturn(true);
|
|
275
|
+
|
|
276
|
+
assertThatThrownBy(() -> userService.create(request))
|
|
277
|
+
.isInstanceOf(DuplicateEmailException.class);
|
|
278
|
+
verify(userRepository, never()).save(any());
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Integration Tests (Controller layer)
|
|
286
|
+
|
|
287
|
+
```java
|
|
288
|
+
\@SpringBootTest
|
|
289
|
+
\@AutoConfigureMockMvc
|
|
290
|
+
class UserControllerIntegrationTest {
|
|
291
|
+
|
|
292
|
+
\@Autowired
|
|
293
|
+
private MockMvc mockMvc;
|
|
294
|
+
|
|
295
|
+
\@Autowired
|
|
296
|
+
private ObjectMapper objectMapper;
|
|
297
|
+
|
|
298
|
+
\@Test
|
|
299
|
+
void createUser_shouldReturn201_whenRequestIsValid() throws Exception {
|
|
300
|
+
CreateUserRequest request = CreateUserRequest.builder()
|
|
301
|
+
.email("test\@example.com")
|
|
302
|
+
.fullName("Test User")
|
|
303
|
+
.password("password123")
|
|
304
|
+
.build();
|
|
305
|
+
|
|
306
|
+
mockMvc.perform(post("/api/v1/users")
|
|
307
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
308
|
+
.content(objectMapper.writeValueAsString(request)))
|
|
309
|
+
.andExpect(status().isCreated())
|
|
310
|
+
.andExpect(jsonPath("$.email").value("test\@example.com"))
|
|
311
|
+
.andExpect(jsonPath("$.id").isNumber());
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
\@Test
|
|
315
|
+
void createUser_shouldReturn400_whenEmailIsInvalid() throws Exception {
|
|
316
|
+
CreateUserRequest request = CreateUserRequest.builder()
|
|
317
|
+
.email("not-an-email")
|
|
318
|
+
.fullName("Test")
|
|
319
|
+
.password("password123")
|
|
320
|
+
.build();
|
|
321
|
+
|
|
322
|
+
mockMvc.perform(post("/api/v1/users")
|
|
323
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
324
|
+
.content(objectMapper.writeValueAsString(request)))
|
|
325
|
+
.andExpect(status().isBadRequest())
|
|
326
|
+
.andExpect(jsonPath("$.code").value("VALIDATION_FAILED"));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
@@ -17,6 +17,13 @@ keywords: spec, plan, implementation, tdd, coding plan
|
|
|
17
17
|
- ✅ Gate 1 APPROVED — `plan/[ticket-id]/requirement.md` reviewed and approved by DEV
|
|
18
18
|
- ✅ Requirement document includes: requirements, solution, impact analysis, estimate
|
|
19
19
|
|
|
20
|
+
## Fast Mode Output Rules (CRITICAL)
|
|
21
|
+
|
|
22
|
+
When updating `plan/[ticket-id]/summary.md` in **fast mode**:
|
|
23
|
+
- **Keep it extremely short.**
|
|
24
|
+
- Add a bullet point indicating Gate 2 plan is ready.
|
|
25
|
+
- **DO NOT copy the entire plan.md into summary.md.**
|
|
26
|
+
|
|
20
27
|
---
|
|
21
28
|
|
|
22
29
|
## Process
|