@yosr-ai/template-spring 0.0.0-stage → 0.2.6
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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +14 -2
- package/features/auth-api-key/files/src/main/java/com/yosr/app/core/ApiKeyFilter.java +52 -0
- package/features/auth-api-key/files/src/test/java/com/yosr/app/ApiKeyTest.java +45 -0
- package/features/auth-api-key/snippets/application-api-key.yaml +1 -0
- package/features/data-jpa/files/dependencies-lock.json +811 -0
- package/features/data-jpa/files/pom.xml +91 -0
- package/features/data-jpa/files/src/main/java/com/yosr/app/core/AppConfig.java +34 -0
- package/features/data-jpa/files/src/main/java/com/yosr/app/core/AppSettings.java +40 -0
- package/features/data-jpa/files/src/main/java/com/yosr/app/core/DataSourceUrl.java +33 -0
- package/features/data-jpa/files/src/main/java/com/yosr/app/features/notes/JpaNoteStore.java +59 -0
- package/features/data-jpa/files/src/main/java/com/yosr/app/features/notes/NoteEntity.java +68 -0
- package/features/data-jpa/files/src/main/java/com/yosr/app/features/notes/NoteRepository.java +5 -0
- package/features/data-jpa/files/src/main/resources/application.yml +14 -0
- package/features/data-jpa/files/src/test/java/com/yosr/app/JpaTestConfig.java +7 -0
- package/features/data-jpa/files/src/test/java/com/yosr/app/NotesApiTest.java +119 -0
- package/features/data-jpa/snippets/readme-db.md +3 -0
- package/features/deploy-docker/files/.dockerignore +5 -0
- package/features/deploy-docker/files/Dockerfile +20 -0
- package/features/deploy-docker/files/compose.yaml +12 -0
- package/features/deploy-docker/snippets/readme-docker.md +3 -0
- package/features/deploy-docker-postgres/snippets/app-config.yaml +3 -0
- package/features/deploy-docker-postgres/snippets/app-env.yaml +1 -0
- package/features/deploy-docker-postgres/snippets/compose-header.yaml +1 -0
- package/features/deploy-docker-postgres/snippets/compose-volumes.yaml +3 -0
- package/features/deploy-docker-postgres/snippets/postgres-service.yaml +13 -0
- package/features/deploy-docker-postgres/snippets/readme-docker.md +3 -0
- package/features/errors-result/files/src/main/java/com/yosr/app/features/notes/NotesController.java +64 -0
- package/features/errors-result/files/src/main/java/com/yosr/app/features/notes/NotesService.java +55 -0
- package/features/errors-result/files/src/main/java/com/yosr/app/shared/Result.java +8 -0
- package/features/errors-result/files/src/test/java/com/yosr/app/NotesServiceTest.java +46 -0
- package/files/.env.example +7 -0
- package/files/.java-version +1 -0
- package/files/README.md +22 -0
- package/files/dependencies-lock.json +547 -0
- package/files/gitignore +9 -0
- package/files/pom.xml +77 -0
- package/files/src/main/java/com/yosr/app/AppApplication.java +12 -0
- package/files/src/main/java/com/yosr/app/core/AppConfig.java +21 -0
- package/files/src/main/java/com/yosr/app/core/AppSettings.java +32 -0
- package/files/src/main/java/com/yosr/app/core/HealthController.java +14 -0
- package/files/src/main/java/com/yosr/app/core/RestExceptionHandler.java +22 -0
- package/files/src/main/java/com/yosr/app/core/WebConfig.java +27 -0
- package/files/src/main/java/com/yosr/app/features/notes/MemoryNoteStore.java +71 -0
- package/files/src/main/java/com/yosr/app/features/notes/Note.java +53 -0
- package/files/src/main/java/com/yosr/app/features/notes/NoteDtos.java +21 -0
- package/files/src/main/java/com/yosr/app/features/notes/NoteStore.java +15 -0
- package/files/src/main/java/com/yosr/app/features/notes/NotesController.java +47 -0
- package/files/src/main/java/com/yosr/app/features/notes/NotesService.java +43 -0
- package/files/src/main/java/com/yosr/app/shared/NotFoundException.java +8 -0
- package/files/src/main/resources/application.yml +7 -0
- package/files/src/test/java/com/yosr/app/NotesApiTest.java +102 -0
- package/files/src/test/java/com/yosr/app/NotesServiceTest.java +42 -0
- package/package.json +26 -4
- package/rules/architecture.md +5 -0
- package/rules/auth-api-key.md +3 -0
- package/rules/commands.md +7 -0
- package/rules/data-jpa.md +3 -0
- package/rules/data-none.md +3 -0
- package/rules/errors-result.md +3 -0
- package/rules/errors-throw.md +3 -0
- package/rules/layout-feature.md +5 -0
- package/rules/layout-flat.md +3 -0
- package/rules/layout-layer.md +3 -0
- package/rules/project-overview.md +5 -0
- package/seed.template.json +711 -0
- package/style/.gitkeep +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package com.yosr.app.core;
|
|
2
|
+
|
|
3
|
+
import java.util.Arrays;
|
|
4
|
+
import java.util.List;
|
|
5
|
+
import org.springframework.beans.factory.annotation.Value;
|
|
6
|
+
import org.springframework.stereotype.Component;
|
|
7
|
+
|
|
8
|
+
@Component
|
|
9
|
+
public class AppSettings {
|
|
10
|
+
private final String appName;
|
|
11
|
+
private final List<String> corsOrigins;
|
|
12
|
+
|
|
13
|
+
// @seed:anchor settings-fields
|
|
14
|
+
public AppSettings(
|
|
15
|
+
@Value("${app.name}") String appName, @Value("${app.cors-origins}") String corsOriginsRaw) {
|
|
16
|
+
this.appName = appName;
|
|
17
|
+
this.corsOrigins =
|
|
18
|
+
Arrays.stream(corsOriginsRaw.split(","))
|
|
19
|
+
.map(String::trim)
|
|
20
|
+
.filter(part -> !part.isEmpty())
|
|
21
|
+
.toList();
|
|
22
|
+
// @seed:anchor settings-values
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public String appName() {
|
|
26
|
+
return appName;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public List<String> corsOrigins() {
|
|
30
|
+
return corsOrigins;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package com.yosr.app.core;
|
|
2
|
+
|
|
3
|
+
import java.util.Map;
|
|
4
|
+
import org.springframework.web.bind.annotation.GetMapping;
|
|
5
|
+
import org.springframework.web.bind.annotation.RestController;
|
|
6
|
+
|
|
7
|
+
@RestController
|
|
8
|
+
public class HealthController {
|
|
9
|
+
|
|
10
|
+
@GetMapping("/health")
|
|
11
|
+
public Map<String, String> health() {
|
|
12
|
+
return Map.of("status", "ok");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.yosr.app.core;
|
|
2
|
+
|
|
3
|
+
import com.yosr.app.shared.NotFoundException;
|
|
4
|
+
import java.util.Map;
|
|
5
|
+
import org.springframework.http.HttpStatus;
|
|
6
|
+
import org.springframework.http.ResponseEntity;
|
|
7
|
+
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
8
|
+
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|
9
|
+
|
|
10
|
+
@RestControllerAdvice
|
|
11
|
+
public class RestExceptionHandler {
|
|
12
|
+
|
|
13
|
+
@ExceptionHandler(NotFoundException.class)
|
|
14
|
+
public ResponseEntity<Map<String, String>> notFound(NotFoundException ex) {
|
|
15
|
+
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("detail", ex.getMessage()));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@ExceptionHandler(IllegalArgumentException.class)
|
|
19
|
+
public ResponseEntity<Map<String, String>> badRequest(IllegalArgumentException ex) {
|
|
20
|
+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of("detail", ex.getMessage()));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package com.yosr.app.core;
|
|
2
|
+
|
|
3
|
+
import java.util.List;
|
|
4
|
+
import org.springframework.context.annotation.Configuration;
|
|
5
|
+
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
|
6
|
+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|
7
|
+
|
|
8
|
+
@Configuration
|
|
9
|
+
public class WebConfig implements WebMvcConfigurer {
|
|
10
|
+
|
|
11
|
+
private final AppSettings settings;
|
|
12
|
+
|
|
13
|
+
public WebConfig(AppSettings settings) {
|
|
14
|
+
this.settings = settings;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Override
|
|
18
|
+
public void addCorsMappings(CorsRegistry registry) {
|
|
19
|
+
// @seed:anchor filter-registration
|
|
20
|
+
List<String> origins = settings.corsOrigins();
|
|
21
|
+
registry
|
|
22
|
+
.addMapping("/**")
|
|
23
|
+
.allowedOrigins(origins.toArray(String[]::new))
|
|
24
|
+
.allowedMethods("GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS")
|
|
25
|
+
.allowedHeaders("*");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package com.yosr.app.features.notes;
|
|
2
|
+
|
|
3
|
+
import java.time.OffsetDateTime;
|
|
4
|
+
import java.util.ArrayList;
|
|
5
|
+
import java.util.Comparator;
|
|
6
|
+
import java.util.List;
|
|
7
|
+
import java.util.Optional;
|
|
8
|
+
import java.util.concurrent.atomic.AtomicInteger;
|
|
9
|
+
|
|
10
|
+
public final class MemoryNoteStore implements NoteStore {
|
|
11
|
+
|
|
12
|
+
private final Object gate = new Object();
|
|
13
|
+
private final List<Note> notes = new ArrayList<>();
|
|
14
|
+
private final AtomicInteger nextId = new AtomicInteger(1);
|
|
15
|
+
|
|
16
|
+
@Override
|
|
17
|
+
public List<Note> list() {
|
|
18
|
+
synchronized (gate) {
|
|
19
|
+
return notes.stream()
|
|
20
|
+
.sorted(Comparator.comparingInt(Note::id).reversed())
|
|
21
|
+
.map(
|
|
22
|
+
note ->
|
|
23
|
+
new Note(
|
|
24
|
+
note.id(), note.title(), note.body(), note.createdAt(), note.updatedAt()))
|
|
25
|
+
.toList();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Override
|
|
30
|
+
public Optional<Note> get(int id) {
|
|
31
|
+
synchronized (gate) {
|
|
32
|
+
return notes.stream()
|
|
33
|
+
.filter(note -> note.id() == id)
|
|
34
|
+
.findFirst()
|
|
35
|
+
.map(
|
|
36
|
+
note ->
|
|
37
|
+
new Note(
|
|
38
|
+
note.id(), note.title(), note.body(), note.createdAt(), note.updatedAt()));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Override
|
|
43
|
+
public Note add(String title, String body) {
|
|
44
|
+
synchronized (gate) {
|
|
45
|
+
var now = OffsetDateTime.now();
|
|
46
|
+
var note = new Note(nextId.getAndIncrement(), title, body, now, null);
|
|
47
|
+
notes.add(note);
|
|
48
|
+
return new Note(note.id(), note.title(), note.body(), note.createdAt(), note.updatedAt());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Override
|
|
53
|
+
public Optional<Note> update(int id, String title, String body) {
|
|
54
|
+
synchronized (gate) {
|
|
55
|
+
for (Note note : notes) {
|
|
56
|
+
if (note.id() == id) {
|
|
57
|
+
if (title != null) {
|
|
58
|
+
note.setTitle(title);
|
|
59
|
+
}
|
|
60
|
+
if (body != null) {
|
|
61
|
+
note.setBody(body);
|
|
62
|
+
}
|
|
63
|
+
note.setUpdatedAt(OffsetDateTime.now());
|
|
64
|
+
return Optional.of(
|
|
65
|
+
new Note(note.id(), note.title(), note.body(), note.createdAt(), note.updatedAt()));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return Optional.empty();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package com.yosr.app.features.notes;
|
|
2
|
+
|
|
3
|
+
import java.time.OffsetDateTime;
|
|
4
|
+
|
|
5
|
+
public final class Note {
|
|
6
|
+
|
|
7
|
+
private final int id;
|
|
8
|
+
private String title;
|
|
9
|
+
private String body;
|
|
10
|
+
private final OffsetDateTime createdAt;
|
|
11
|
+
private OffsetDateTime updatedAt;
|
|
12
|
+
|
|
13
|
+
public Note(
|
|
14
|
+
int id, String title, String body, OffsetDateTime createdAt, OffsetDateTime updatedAt) {
|
|
15
|
+
this.id = id;
|
|
16
|
+
this.title = title;
|
|
17
|
+
this.body = body;
|
|
18
|
+
this.createdAt = createdAt;
|
|
19
|
+
this.updatedAt = updatedAt;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public int id() {
|
|
23
|
+
return id;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public String title() {
|
|
27
|
+
return title;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public void setTitle(String title) {
|
|
31
|
+
this.title = title;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public String body() {
|
|
35
|
+
return body;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public void setBody(String body) {
|
|
39
|
+
this.body = body;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public OffsetDateTime createdAt() {
|
|
43
|
+
return createdAt;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public OffsetDateTime updatedAt() {
|
|
47
|
+
return updatedAt;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public void setUpdatedAt(OffsetDateTime updatedAt) {
|
|
51
|
+
this.updatedAt = updatedAt;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
package com.yosr.app.features.notes;
|
|
2
|
+
|
|
3
|
+
import java.time.OffsetDateTime;
|
|
4
|
+
|
|
5
|
+
public final class NoteDtos {
|
|
6
|
+
|
|
7
|
+
private NoteDtos() {}
|
|
8
|
+
|
|
9
|
+
public record NoteCreate(String title, String body) {}
|
|
10
|
+
|
|
11
|
+
public record NoteUpdate(String title, String body) {}
|
|
12
|
+
|
|
13
|
+
public record NoteResponse(
|
|
14
|
+
int id, String title, String body, OffsetDateTime createdAt, OffsetDateTime updatedAt) {
|
|
15
|
+
|
|
16
|
+
public static NoteResponse from(Note note) {
|
|
17
|
+
return new NoteResponse(
|
|
18
|
+
note.id(), note.title(), note.body(), note.createdAt(), note.updatedAt());
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package com.yosr.app.features.notes;
|
|
2
|
+
|
|
3
|
+
import java.util.List;
|
|
4
|
+
import java.util.Optional;
|
|
5
|
+
|
|
6
|
+
public interface NoteStore {
|
|
7
|
+
|
|
8
|
+
List<Note> list();
|
|
9
|
+
|
|
10
|
+
Optional<Note> get(int id);
|
|
11
|
+
|
|
12
|
+
Note add(String title, String body);
|
|
13
|
+
|
|
14
|
+
Optional<Note> update(int id, String title, String body);
|
|
15
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package com.yosr.app.features.notes;
|
|
2
|
+
|
|
3
|
+
import com.yosr.app.features.notes.NoteDtos.NoteCreate;
|
|
4
|
+
import com.yosr.app.features.notes.NoteDtos.NoteResponse;
|
|
5
|
+
import com.yosr.app.features.notes.NoteDtos.NoteUpdate;
|
|
6
|
+
import java.net.URI;
|
|
7
|
+
import java.util.List;
|
|
8
|
+
import org.springframework.http.ResponseEntity;
|
|
9
|
+
import org.springframework.web.bind.annotation.GetMapping;
|
|
10
|
+
import org.springframework.web.bind.annotation.PatchMapping;
|
|
11
|
+
import org.springframework.web.bind.annotation.PathVariable;
|
|
12
|
+
import org.springframework.web.bind.annotation.PostMapping;
|
|
13
|
+
import org.springframework.web.bind.annotation.RequestBody;
|
|
14
|
+
import org.springframework.web.bind.annotation.RequestMapping;
|
|
15
|
+
import org.springframework.web.bind.annotation.RestController;
|
|
16
|
+
|
|
17
|
+
@RestController
|
|
18
|
+
@RequestMapping("/notes")
|
|
19
|
+
public class NotesController {
|
|
20
|
+
private final NotesService notes;
|
|
21
|
+
|
|
22
|
+
public NotesController(NotesService notes) {
|
|
23
|
+
this.notes = notes;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// @seed:anchor notes-group
|
|
27
|
+
@GetMapping
|
|
28
|
+
public List<NoteResponse> list() {
|
|
29
|
+
return notes.list().stream().map(NoteResponse::from).toList();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@GetMapping("/{id}")
|
|
33
|
+
public NoteResponse get(@PathVariable int id) {
|
|
34
|
+
return NoteResponse.from(notes.get(id));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@PostMapping
|
|
38
|
+
public ResponseEntity<NoteResponse> create(@RequestBody NoteCreate body) {
|
|
39
|
+
Note note = notes.create(body);
|
|
40
|
+
return ResponseEntity.created(URI.create("/notes/" + note.id())).body(NoteResponse.from(note));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@PatchMapping("/{id}")
|
|
44
|
+
public NoteResponse update(@PathVariable int id, @RequestBody NoteUpdate body) {
|
|
45
|
+
return NoteResponse.from(notes.update(id, body));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package com.yosr.app.features.notes;
|
|
2
|
+
|
|
3
|
+
import com.yosr.app.shared.NotFoundException;
|
|
4
|
+
import java.util.List;
|
|
5
|
+
|
|
6
|
+
public final class NotesService {
|
|
7
|
+
|
|
8
|
+
private final NoteStore store;
|
|
9
|
+
|
|
10
|
+
public NotesService(NoteStore store) {
|
|
11
|
+
this.store = store;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public List<Note> list() {
|
|
15
|
+
return store.list();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public Note get(int id) {
|
|
19
|
+
return store.get(id).orElseThrow(() -> new NotFoundException("Note " + id + " was not found."));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public Note create(NoteDtos.NoteCreate input) {
|
|
23
|
+
String title = requireTitle(input.title());
|
|
24
|
+
return store.add(title, input.body());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public Note update(int id, NoteDtos.NoteUpdate input) {
|
|
28
|
+
if (input.title() != null) {
|
|
29
|
+
requireTitle(input.title());
|
|
30
|
+
}
|
|
31
|
+
return store
|
|
32
|
+
.update(id, input.title(), input.body())
|
|
33
|
+
.orElseThrow(() -> new NotFoundException("Note " + id + " was not found."));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private static String requireTitle(String title) {
|
|
37
|
+
String trimmed = title == null ? "" : title.trim();
|
|
38
|
+
if (trimmed.isEmpty()) {
|
|
39
|
+
throw new IllegalArgumentException("Title must not be blank.");
|
|
40
|
+
}
|
|
41
|
+
return trimmed;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
package com.yosr.app;
|
|
2
|
+
|
|
3
|
+
import static org.hamcrest.Matchers.hasSize;
|
|
4
|
+
import static org.hamcrest.Matchers.is;
|
|
5
|
+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
6
|
+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
|
|
7
|
+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
|
8
|
+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
9
|
+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
10
|
+
|
|
11
|
+
import org.junit.jupiter.api.BeforeEach;
|
|
12
|
+
import org.junit.jupiter.api.Test;
|
|
13
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
14
|
+
import org.springframework.boot.test.context.SpringBootTest;
|
|
15
|
+
import org.springframework.http.MediaType;
|
|
16
|
+
import org.springframework.test.web.servlet.MockMvc;
|
|
17
|
+
import org.springframework.test.web.servlet.MvcResult;
|
|
18
|
+
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
|
19
|
+
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
20
|
+
import org.springframework.web.context.WebApplicationContext;
|
|
21
|
+
import tools.jackson.databind.JsonNode;
|
|
22
|
+
import tools.jackson.databind.ObjectMapper;
|
|
23
|
+
|
|
24
|
+
// @seed:anchor test-class-annotations
|
|
25
|
+
@SpringBootTest
|
|
26
|
+
class NotesApiTest {
|
|
27
|
+
|
|
28
|
+
@Autowired private WebApplicationContext context;
|
|
29
|
+
|
|
30
|
+
@Autowired private ObjectMapper objectMapper;
|
|
31
|
+
|
|
32
|
+
private MockMvc mockMvc;
|
|
33
|
+
|
|
34
|
+
// @seed:anchor test-client-headers
|
|
35
|
+
@BeforeEach
|
|
36
|
+
void resetHeaders() {
|
|
37
|
+
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
|
|
38
|
+
// @seed:anchor test-setup
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private MockHttpServletRequestBuilder forNotes(MockHttpServletRequestBuilder builder) {
|
|
42
|
+
// @seed:anchor notes-request-wrap
|
|
43
|
+
return builder;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
void healthIsOk() throws Exception {
|
|
48
|
+
mockMvc
|
|
49
|
+
.perform(get("/health"))
|
|
50
|
+
.andExpect(status().isOk())
|
|
51
|
+
.andExpect(jsonPath("$.status", is("ok")));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test
|
|
55
|
+
void createListGetAndUpdateANote() throws Exception {
|
|
56
|
+
MvcResult created =
|
|
57
|
+
mockMvc
|
|
58
|
+
.perform(
|
|
59
|
+
forNotes(post("/notes"))
|
|
60
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
61
|
+
.content("{\"title\":\"Hello\",\"body\":\"World\"}"))
|
|
62
|
+
.andExpect(status().isCreated())
|
|
63
|
+
.andReturn();
|
|
64
|
+
|
|
65
|
+
JsonNode note = objectMapper.readTree(created.getResponse().getContentAsByteArray());
|
|
66
|
+
int id = note.get("id").asInt();
|
|
67
|
+
|
|
68
|
+
mockMvc
|
|
69
|
+
.perform(forNotes(get("/notes")))
|
|
70
|
+
.andExpect(status().isOk())
|
|
71
|
+
.andExpect(jsonPath("$", hasSize(1)));
|
|
72
|
+
|
|
73
|
+
mockMvc
|
|
74
|
+
.perform(forNotes(get("/notes/" + id)))
|
|
75
|
+
.andExpect(status().isOk())
|
|
76
|
+
.andExpect(jsonPath("$.title", is("Hello")));
|
|
77
|
+
|
|
78
|
+
mockMvc
|
|
79
|
+
.perform(
|
|
80
|
+
forNotes(patch("/notes/" + id))
|
|
81
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
82
|
+
.content("{\"title\":\"Hi\"}"))
|
|
83
|
+
.andExpect(status().isOk())
|
|
84
|
+
.andExpect(jsonPath("$.title", is("Hi")))
|
|
85
|
+
.andExpect(jsonPath("$.body", is("World")));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Test
|
|
89
|
+
void missingNoteIs404() throws Exception {
|
|
90
|
+
mockMvc.perform(forNotes(get("/notes/999"))).andExpect(status().isNotFound());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Test
|
|
94
|
+
void blankTitleIsRejected() throws Exception {
|
|
95
|
+
mockMvc
|
|
96
|
+
.perform(
|
|
97
|
+
forNotes(post("/notes"))
|
|
98
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
99
|
+
.content("{\"title\":\" \",\"body\":\"x\"}"))
|
|
100
|
+
.andExpect(status().isBadRequest());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package com.yosr.app;
|
|
2
|
+
|
|
3
|
+
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
4
|
+
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
5
|
+
|
|
6
|
+
import com.yosr.app.features.notes.MemoryNoteStore;
|
|
7
|
+
import com.yosr.app.features.notes.NoteDtos.NoteCreate;
|
|
8
|
+
import com.yosr.app.features.notes.NoteDtos.NoteUpdate;
|
|
9
|
+
import com.yosr.app.features.notes.NotesService;
|
|
10
|
+
import com.yosr.app.shared.NotFoundException;
|
|
11
|
+
import org.junit.jupiter.api.Test;
|
|
12
|
+
|
|
13
|
+
class NotesServiceTest {
|
|
14
|
+
|
|
15
|
+
private static NotesService service() {
|
|
16
|
+
return new NotesService(new MemoryNoteStore());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Test
|
|
20
|
+
void createThenGetReturnsTheNote() {
|
|
21
|
+
NotesService notes = service();
|
|
22
|
+
var created = notes.create(new NoteCreate("Hello", "Body"));
|
|
23
|
+
var found = notes.get(created.id());
|
|
24
|
+
assertEquals("Hello", found.title());
|
|
25
|
+
assertEquals("Body", found.body());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Test
|
|
29
|
+
void getMissingNoteThrows() {
|
|
30
|
+
NotesService notes = service();
|
|
31
|
+
assertThrows(NotFoundException.class, () -> notes.get(99));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Test
|
|
35
|
+
void updateChangesOnlyGivenFields() {
|
|
36
|
+
NotesService notes = service();
|
|
37
|
+
var created = notes.create(new NoteCreate("Hello", "Body"));
|
|
38
|
+
var updated = notes.update(created.id(), new NoteUpdate("Hi", null));
|
|
39
|
+
assertEquals("Hi", updated.title());
|
|
40
|
+
assertEquals("Body", updated.body());
|
|
41
|
+
}
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yosr-ai/template-spring",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.6",
|
|
4
|
+
"description": "Yosr Seed Spring Boot template.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://seed.yosr.ai",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"email": "wzohny@gmail.com"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
"./package.json": "./package.json",
|
|
13
|
+
"./seed.template.json": "./seed.template.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"README.md",
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"seed.template.json",
|
|
20
|
+
"files",
|
|
21
|
+
"features",
|
|
22
|
+
"style",
|
|
23
|
+
"rules"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=24"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Spring Boot REST on Java 25. `AppApplication` boots component scanning under `com.yosr.app`. A request hits `features/<feature>/` controllers, which call a service; the service talks to a `NoteStore` implementation (memory or JPA). Settings come from `application.yml` and environment variables via `AppSettings`.
|
|
4
|
+
|
|
5
|
+
Unit tests exercise services with a memory store. HTTP tests use MockMvc in-process. Gate never runs schema migrations; Postgres reachability is via the gate service / `--database-url`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Commands
|
|
2
|
+
|
|
3
|
+
- `mvn -B -q dependency:resolve dependency-lock:check` — locked Maven resolve from `dependencies-lock.json`
|
|
4
|
+
- `mvn -B -q spring-boot:run` — run the API
|
|
5
|
+
- `mvn -B -q spotless:check` — Google Java Format (Spotless)
|
|
6
|
+
- `mvn -B -q compile` — compile (`-Werror` on the compiler)
|
|
7
|
+
- `mvn -B -q test` — JUnit 5 + MockMvc
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Data (JPA / Postgres)
|
|
2
|
+
|
|
3
|
+
__seed_storageLabel__ through Spring Data JPA. `AppSettings` and `application.yml` read `DATABASE_URL`. Gate only checks reachability (throwaway container or `--database-url`); it does not run Flyway or Liquibase. Tests use `ddl-auto=create-drop` against throwaway Postgres or embedded H2.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Layout (feature)
|
|
2
|
+
|
|
3
|
+
- `__seed_domainDir__/notes/` holds that feature's models, store, service, and controller. Follow `notes/`.
|
|
4
|
+
- `__seed_serverDir__/` holds app settings, config, filters, and health.
|
|
5
|
+
- `__seed_sharedDir__/` holds cross-cutting types (errors, Result).
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# __seed_projectName__
|
|
2
|
+
|
|
3
|
+
Spring Boot REST API on Java 25. Data: __seed_storageLabel__. Auth: __seed_authLabel__. Deploy: __seed_deploy__.
|
|
4
|
+
|
|
5
|
+
Install with `mvn -B -q dependency:resolve dependency-lock:check` (committed `dependencies-lock.json`). Format with `mvn spotless:check`. Compile with `mvn compile` (warnings fail). Test with `mvn test` (JUnit 5 + MockMvc, no live server). Maven coordinates stay `com.yosr:app`.
|