@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +14 -2
  4. package/features/auth-api-key/files/src/main/java/com/yosr/app/core/ApiKeyFilter.java +52 -0
  5. package/features/auth-api-key/files/src/test/java/com/yosr/app/ApiKeyTest.java +45 -0
  6. package/features/auth-api-key/snippets/application-api-key.yaml +1 -0
  7. package/features/data-jpa/files/dependencies-lock.json +811 -0
  8. package/features/data-jpa/files/pom.xml +91 -0
  9. package/features/data-jpa/files/src/main/java/com/yosr/app/core/AppConfig.java +34 -0
  10. package/features/data-jpa/files/src/main/java/com/yosr/app/core/AppSettings.java +40 -0
  11. package/features/data-jpa/files/src/main/java/com/yosr/app/core/DataSourceUrl.java +33 -0
  12. package/features/data-jpa/files/src/main/java/com/yosr/app/features/notes/JpaNoteStore.java +59 -0
  13. package/features/data-jpa/files/src/main/java/com/yosr/app/features/notes/NoteEntity.java +68 -0
  14. package/features/data-jpa/files/src/main/java/com/yosr/app/features/notes/NoteRepository.java +5 -0
  15. package/features/data-jpa/files/src/main/resources/application.yml +14 -0
  16. package/features/data-jpa/files/src/test/java/com/yosr/app/JpaTestConfig.java +7 -0
  17. package/features/data-jpa/files/src/test/java/com/yosr/app/NotesApiTest.java +119 -0
  18. package/features/data-jpa/snippets/readme-db.md +3 -0
  19. package/features/deploy-docker/files/.dockerignore +5 -0
  20. package/features/deploy-docker/files/Dockerfile +20 -0
  21. package/features/deploy-docker/files/compose.yaml +12 -0
  22. package/features/deploy-docker/snippets/readme-docker.md +3 -0
  23. package/features/deploy-docker-postgres/snippets/app-config.yaml +3 -0
  24. package/features/deploy-docker-postgres/snippets/app-env.yaml +1 -0
  25. package/features/deploy-docker-postgres/snippets/compose-header.yaml +1 -0
  26. package/features/deploy-docker-postgres/snippets/compose-volumes.yaml +3 -0
  27. package/features/deploy-docker-postgres/snippets/postgres-service.yaml +13 -0
  28. package/features/deploy-docker-postgres/snippets/readme-docker.md +3 -0
  29. package/features/errors-result/files/src/main/java/com/yosr/app/features/notes/NotesController.java +64 -0
  30. package/features/errors-result/files/src/main/java/com/yosr/app/features/notes/NotesService.java +55 -0
  31. package/features/errors-result/files/src/main/java/com/yosr/app/shared/Result.java +8 -0
  32. package/features/errors-result/files/src/test/java/com/yosr/app/NotesServiceTest.java +46 -0
  33. package/files/.env.example +7 -0
  34. package/files/.java-version +1 -0
  35. package/files/README.md +22 -0
  36. package/files/dependencies-lock.json +547 -0
  37. package/files/gitignore +9 -0
  38. package/files/pom.xml +77 -0
  39. package/files/src/main/java/com/yosr/app/AppApplication.java +12 -0
  40. package/files/src/main/java/com/yosr/app/core/AppConfig.java +21 -0
  41. package/files/src/main/java/com/yosr/app/core/AppSettings.java +32 -0
  42. package/files/src/main/java/com/yosr/app/core/HealthController.java +14 -0
  43. package/files/src/main/java/com/yosr/app/core/RestExceptionHandler.java +22 -0
  44. package/files/src/main/java/com/yosr/app/core/WebConfig.java +27 -0
  45. package/files/src/main/java/com/yosr/app/features/notes/MemoryNoteStore.java +71 -0
  46. package/files/src/main/java/com/yosr/app/features/notes/Note.java +53 -0
  47. package/files/src/main/java/com/yosr/app/features/notes/NoteDtos.java +21 -0
  48. package/files/src/main/java/com/yosr/app/features/notes/NoteStore.java +15 -0
  49. package/files/src/main/java/com/yosr/app/features/notes/NotesController.java +47 -0
  50. package/files/src/main/java/com/yosr/app/features/notes/NotesService.java +43 -0
  51. package/files/src/main/java/com/yosr/app/shared/NotFoundException.java +8 -0
  52. package/files/src/main/resources/application.yml +7 -0
  53. package/files/src/test/java/com/yosr/app/NotesApiTest.java +102 -0
  54. package/files/src/test/java/com/yosr/app/NotesServiceTest.java +42 -0
  55. package/package.json +26 -4
  56. package/rules/architecture.md +5 -0
  57. package/rules/auth-api-key.md +3 -0
  58. package/rules/commands.md +7 -0
  59. package/rules/data-jpa.md +3 -0
  60. package/rules/data-none.md +3 -0
  61. package/rules/errors-result.md +3 -0
  62. package/rules/errors-throw.md +3 -0
  63. package/rules/layout-feature.md +5 -0
  64. package/rules/layout-flat.md +3 -0
  65. package/rules/layout-layer.md +3 -0
  66. package/rules/project-overview.md +5 -0
  67. package/seed.template.json +711 -0
  68. 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,8 @@
1
+ package com.yosr.app.shared;
2
+
3
+ public class NotFoundException extends RuntimeException {
4
+
5
+ public NotFoundException(String message) {
6
+ super(message);
7
+ }
8
+ }
@@ -0,0 +1,7 @@
1
+ server:
2
+ port: 8080
3
+
4
+ app:
5
+ name: ${APP_NAME:__seed_projectName__}
6
+ cors-origins: ${CORS_ORIGINS:http://localhost:5173}
7
+ # @seed:anchor application-config
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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,3 @@
1
+ # Auth (API key)
2
+
3
+ Callers send `X-API-Key` matching the `API_KEY` environment variable (`app.api-key`). `/health` stays open. Set `API_KEY` in `.env` for local runs.
@@ -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,3 @@
1
+ # Data (memory)
2
+
3
+ Notes live in an in-process `MemoryNoteStore`. Restarting the JVM clears data. No database URL is required.
@@ -0,0 +1,3 @@
1
+ # Errors (Result)
2
+
3
+ Services return `Result.Ok` / `Result.Err` instead of throwing for expected failures. Controllers map `Err` to HTTP status codes.
@@ -0,0 +1,3 @@
1
+ # Errors (throw)
2
+
3
+ Expected failures raise typed exceptions (for example `NotFoundException`). `RestExceptionHandler` maps them to HTTP status codes.
@@ -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,3 @@
1
+ # Layout (flat)
2
+
3
+ - Feature types live under the project root package (`__seed_domainDir__/`). Keep `core` and `shared` for infrastructure and shared types.
@@ -0,0 +1,3 @@
1
+ # Layout (layer)
2
+
3
+ - Domain services live under `__seed_domainDir__/`. Shared types under `__seed_sharedDir__/`. Core/infrastructure under `__seed_serverDir__/`.
@@ -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`.