@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,91 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project xmlns="http://maven.apache.org/POM/4.0.0"
3
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5
+ <modelVersion>4.0.0</modelVersion>
6
+
7
+ <parent>
8
+ <groupId>org.springframework.boot</groupId>
9
+ <artifactId>spring-boot-starter-parent</artifactId>
10
+ <version>4.1.2</version>
11
+ <relativePath />
12
+ </parent>
13
+
14
+ <groupId>com.yosr</groupId>
15
+ <artifactId>app</artifactId>
16
+ <version>0.0.1-SNAPSHOT</version>
17
+ <name>__seed_projectName__</name>
18
+ <description>Yosr Seed Spring Boot API (__seed_projectName__)</description>
19
+
20
+ <properties>
21
+ <java.version>25</java.version>
22
+ <dependency-lock-maven-plugin.version>1.4.0</dependency-lock-maven-plugin.version>
23
+ <spotless-maven-plugin.version>3.11.0</spotless-maven-plugin.version>
24
+ </properties>
25
+
26
+ <dependencies>
27
+ <dependency>
28
+ <groupId>org.springframework.boot</groupId>
29
+ <artifactId>spring-boot-starter-web</artifactId>
30
+ </dependency>
31
+ <dependency>
32
+ <groupId>org.springframework.boot</groupId>
33
+ <artifactId>spring-boot-starter-data-jpa</artifactId>
34
+ </dependency>
35
+ <dependency>
36
+ <groupId>org.postgresql</groupId>
37
+ <artifactId>postgresql</artifactId>
38
+ <scope>runtime</scope>
39
+ </dependency>
40
+ <dependency>
41
+ <groupId>org.springframework.boot</groupId>
42
+ <artifactId>spring-boot-starter-test</artifactId>
43
+ <scope>test</scope>
44
+ </dependency>
45
+ <dependency>
46
+ <groupId>com.h2database</groupId>
47
+ <artifactId>h2</artifactId>
48
+ <scope>test</scope>
49
+ </dependency>
50
+ </dependencies>
51
+
52
+ <build>
53
+ <finalName>app</finalName>
54
+ <plugins>
55
+ <plugin>
56
+ <groupId>org.springframework.boot</groupId>
57
+ <artifactId>spring-boot-maven-plugin</artifactId>
58
+ </plugin>
59
+ <plugin>
60
+ <groupId>org.apache.maven.plugins</groupId>
61
+ <artifactId>maven-compiler-plugin</artifactId>
62
+ <configuration>
63
+ <compilerArgs>
64
+ <arg>-Werror</arg>
65
+ </compilerArgs>
66
+ <showWarnings>true</showWarnings>
67
+ </configuration>
68
+ </plugin>
69
+ <plugin>
70
+ <groupId>com.diffplug.spotless</groupId>
71
+ <artifactId>spotless-maven-plugin</artifactId>
72
+ <version>${spotless-maven-plugin.version}</version>
73
+ <configuration>
74
+ <java>
75
+ <googleJavaFormat>
76
+ <version>1.36.1</version>
77
+ </googleJavaFormat>
78
+ </java>
79
+ </configuration>
80
+ </plugin>
81
+ <plugin>
82
+ <groupId>se.vandmo</groupId>
83
+ <artifactId>dependency-lock-maven-plugin</artifactId>
84
+ <version>${dependency-lock-maven-plugin.version}</version>
85
+ <configuration>
86
+ <lockFile>dependencies-lock.json</lockFile>
87
+ </configuration>
88
+ </plugin>
89
+ </plugins>
90
+ </build>
91
+ </project>
@@ -0,0 +1,34 @@
1
+ package com.yosr.app.core;
2
+
3
+ import com.yosr.app.features.notes.JpaNoteStore;
4
+ import com.yosr.app.features.notes.NoteRepository;
5
+ import com.yosr.app.features.notes.NoteStore;
6
+ import com.yosr.app.features.notes.NotesService;
7
+ import com.zaxxer.hikari.HikariDataSource;
8
+ import javax.sql.DataSource;
9
+ import org.springframework.context.annotation.Bean;
10
+ import org.springframework.context.annotation.Configuration;
11
+
12
+ @Configuration
13
+ public class AppConfig {
14
+ // @seed:anchor bean-registration
15
+ @Bean
16
+ DataSource dataSource(AppSettings settings) {
17
+ DataSourceUrl parsed = DataSourceUrl.fromDatabaseUrl(settings.databaseUrl());
18
+ HikariDataSource dataSource = new HikariDataSource();
19
+ dataSource.setJdbcUrl(parsed.jdbcUrl());
20
+ dataSource.setUsername(parsed.username());
21
+ dataSource.setPassword(parsed.password());
22
+ return dataSource;
23
+ }
24
+
25
+ @Bean
26
+ NoteStore noteStore(NoteRepository repository) {
27
+ return new JpaNoteStore(repository);
28
+ }
29
+
30
+ @Bean
31
+ NotesService notesService(NoteStore store) {
32
+ return new NotesService(store);
33
+ }
34
+ }
@@ -0,0 +1,40 @@
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
+ private final String databaseUrl;
13
+
14
+ // @seed:anchor settings-fields
15
+ public AppSettings(
16
+ @Value("${app.name}") String appName,
17
+ @Value("${app.cors-origins}") String corsOriginsRaw,
18
+ @Value("${app.database-url}") String databaseUrl) {
19
+ this.appName = appName;
20
+ this.corsOrigins =
21
+ Arrays.stream(corsOriginsRaw.split(","))
22
+ .map(String::trim)
23
+ .filter(part -> !part.isEmpty())
24
+ .toList();
25
+ this.databaseUrl = databaseUrl;
26
+ // @seed:anchor settings-values
27
+ }
28
+
29
+ public String appName() {
30
+ return appName;
31
+ }
32
+
33
+ public List<String> corsOrigins() {
34
+ return corsOrigins;
35
+ }
36
+
37
+ public String databaseUrl() {
38
+ return databaseUrl;
39
+ }
40
+ }
@@ -0,0 +1,33 @@
1
+ package com.yosr.app.core;
2
+
3
+ import java.net.URI;
4
+ import java.net.URLDecoder;
5
+ import java.nio.charset.StandardCharsets;
6
+
7
+ public record DataSourceUrl(String jdbcUrl, String username, String password) {
8
+
9
+ public static DataSourceUrl fromDatabaseUrl(String databaseUrl) {
10
+ if (databaseUrl.startsWith("postgresql://") || databaseUrl.startsWith("postgres://")) {
11
+ URI uri = URI.create(databaseUrl);
12
+ String username = "";
13
+ String password = "";
14
+ String userInfo = uri.getUserInfo();
15
+ if (userInfo != null && userInfo.contains(":")) {
16
+ String[] parts = userInfo.split(":", 2);
17
+ username = decode(parts[0]);
18
+ password = decode(parts[1]);
19
+ } else if (userInfo != null) {
20
+ username = decode(userInfo);
21
+ }
22
+ String db = uri.getPath() == null ? "" : uri.getPath().replaceFirst("^/", "");
23
+ int port = uri.getPort() > 0 ? uri.getPort() : 5432;
24
+ String jdbc = "jdbc:postgresql://" + uri.getHost() + ":" + port + "/" + db;
25
+ return new DataSourceUrl(jdbc, username, password);
26
+ }
27
+ return new DataSourceUrl(databaseUrl, "", "");
28
+ }
29
+
30
+ private static String decode(String value) {
31
+ return URLDecoder.decode(value, StandardCharsets.UTF_8);
32
+ }
33
+ }
@@ -0,0 +1,59 @@
1
+ package com.yosr.app.features.notes;
2
+
3
+ import java.time.OffsetDateTime;
4
+ import java.util.List;
5
+ import java.util.Optional;
6
+ import org.springframework.transaction.annotation.Transactional;
7
+
8
+ public class JpaNoteStore implements NoteStore {
9
+
10
+ private final NoteRepository repository;
11
+
12
+ public JpaNoteStore(NoteRepository repository) {
13
+ this.repository = repository;
14
+ }
15
+
16
+ @Override
17
+ @Transactional(readOnly = true)
18
+ public List<Note> list() {
19
+ return repository.findAll().stream()
20
+ .sorted((a, b) -> Integer.compare(b.getId(), a.getId()))
21
+ .map(NoteEntity::toNote)
22
+ .toList();
23
+ }
24
+
25
+ @Override
26
+ @Transactional(readOnly = true)
27
+ public Optional<Note> get(int id) {
28
+ return repository.findById(id).map(NoteEntity::toNote);
29
+ }
30
+
31
+ @Override
32
+ @Transactional
33
+ public Note add(String title, String body) {
34
+ NoteEntity row = new NoteEntity();
35
+ row.setTitle(title);
36
+ row.setBody(body);
37
+ row.setCreatedAt(OffsetDateTime.now());
38
+ NoteEntity saved = repository.save(row);
39
+ return saved.toNote();
40
+ }
41
+
42
+ @Override
43
+ @Transactional
44
+ public Optional<Note> update(int id, String title, String body) {
45
+ return repository
46
+ .findById(id)
47
+ .map(
48
+ row -> {
49
+ if (title != null) {
50
+ row.setTitle(title);
51
+ }
52
+ if (body != null) {
53
+ row.setBody(body);
54
+ }
55
+ row.setUpdatedAt(OffsetDateTime.now());
56
+ return repository.save(row).toNote();
57
+ });
58
+ }
59
+ }
@@ -0,0 +1,68 @@
1
+ package com.yosr.app.features.notes;
2
+
3
+ import jakarta.persistence.Column;
4
+ import jakarta.persistence.Entity;
5
+ import jakarta.persistence.GeneratedValue;
6
+ import jakarta.persistence.GenerationType;
7
+ import jakarta.persistence.Id;
8
+ import jakarta.persistence.Table;
9
+ import java.time.OffsetDateTime;
10
+
11
+ @Entity
12
+ @Table(name = "notes")
13
+ public class NoteEntity {
14
+
15
+ @Id
16
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
17
+ private int id;
18
+
19
+ @Column(nullable = false, length = 200)
20
+ private String title;
21
+
22
+ private String body;
23
+
24
+ @Column(nullable = false)
25
+ private OffsetDateTime createdAt;
26
+
27
+ private OffsetDateTime updatedAt;
28
+
29
+ public int getId() {
30
+ return id;
31
+ }
32
+
33
+ public String getTitle() {
34
+ return title;
35
+ }
36
+
37
+ public void setTitle(String title) {
38
+ this.title = title;
39
+ }
40
+
41
+ public String getBody() {
42
+ return body;
43
+ }
44
+
45
+ public void setBody(String body) {
46
+ this.body = body;
47
+ }
48
+
49
+ public OffsetDateTime getCreatedAt() {
50
+ return createdAt;
51
+ }
52
+
53
+ public void setCreatedAt(OffsetDateTime createdAt) {
54
+ this.createdAt = createdAt;
55
+ }
56
+
57
+ public OffsetDateTime getUpdatedAt() {
58
+ return updatedAt;
59
+ }
60
+
61
+ public void setUpdatedAt(OffsetDateTime updatedAt) {
62
+ this.updatedAt = updatedAt;
63
+ }
64
+
65
+ Note toNote() {
66
+ return new Note(id, title, body, createdAt, updatedAt);
67
+ }
68
+ }
@@ -0,0 +1,5 @@
1
+ package com.yosr.app.features.notes;
2
+
3
+ import org.springframework.data.jpa.repository.JpaRepository;
4
+
5
+ public interface NoteRepository extends JpaRepository<NoteEntity, Integer> {}
@@ -0,0 +1,14 @@
1
+ server:
2
+ port: 8080
3
+
4
+ app:
5
+ name: ${APP_NAME:__seed_projectName__}
6
+ cors-origins: ${CORS_ORIGINS:http://localhost:5173}
7
+ database-url: ${DATABASE_URL:__seed_databaseUrl__}
8
+
9
+ spring:
10
+ jpa:
11
+ hibernate:
12
+ ddl-auto: validate
13
+ open-in-view: false
14
+ # @seed:anchor application-config
@@ -0,0 +1,7 @@
1
+ package com.yosr.app;
2
+
3
+ import org.springframework.boot.test.context.TestConfiguration;
4
+
5
+ /** Marker import for JPA integration tests (properties come from NotesApiTest). */
6
+ @TestConfiguration
7
+ public class JpaTestConfig {}
@@ -0,0 +1,119 @@
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.context.annotation.Import;
16
+ import org.springframework.http.MediaType;
17
+ import org.springframework.test.context.DynamicPropertyRegistry;
18
+ import org.springframework.test.context.DynamicPropertySource;
19
+ import org.springframework.test.web.servlet.MockMvc;
20
+ import org.springframework.test.web.servlet.MvcResult;
21
+ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
22
+ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
23
+ import org.springframework.web.context.WebApplicationContext;
24
+ import tools.jackson.databind.JsonNode;
25
+ import tools.jackson.databind.ObjectMapper;
26
+
27
+ // @seed:anchor test-class-annotations
28
+ @SpringBootTest
29
+ @Import(JpaTestConfig.class)
30
+ class NotesApiTest {
31
+
32
+ @DynamicPropertySource
33
+ static void datasource(DynamicPropertyRegistry registry) {
34
+ String databaseUrl = System.getenv("DATABASE_URL");
35
+ if (databaseUrl != null && !databaseUrl.isBlank()) {
36
+ registry.add("app.database-url", () -> databaseUrl);
37
+ } else {
38
+ registry.add(
39
+ "app.database-url",
40
+ () -> "jdbc:h2:mem:yosr-tests-" + System.nanoTime() + ";DB_CLOSE_DELAY=-1");
41
+ }
42
+ registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop");
43
+ }
44
+
45
+ @Autowired private WebApplicationContext context;
46
+
47
+ @Autowired private ObjectMapper objectMapper;
48
+
49
+ private MockMvc mockMvc;
50
+
51
+ // @seed:anchor test-client-headers
52
+ @BeforeEach
53
+ void resetHeaders() {
54
+ mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
55
+ // @seed:anchor test-setup
56
+ }
57
+
58
+ private MockHttpServletRequestBuilder forNotes(MockHttpServletRequestBuilder builder) {
59
+ // @seed:anchor notes-request-wrap
60
+ return builder;
61
+ }
62
+
63
+ @Test
64
+ void healthIsOk() throws Exception {
65
+ mockMvc
66
+ .perform(get("/health"))
67
+ .andExpect(status().isOk())
68
+ .andExpect(jsonPath("$.status", is("ok")));
69
+ }
70
+
71
+ @Test
72
+ void createListGetAndUpdateANote() throws Exception {
73
+ MvcResult created =
74
+ mockMvc
75
+ .perform(
76
+ forNotes(post("/notes"))
77
+ .contentType(MediaType.APPLICATION_JSON)
78
+ .content("{\"title\":\"Hello\",\"body\":\"World\"}"))
79
+ .andExpect(status().isCreated())
80
+ .andReturn();
81
+
82
+ JsonNode note = objectMapper.readTree(created.getResponse().getContentAsByteArray());
83
+ int id = note.get("id").asInt();
84
+
85
+ mockMvc
86
+ .perform(forNotes(get("/notes")))
87
+ .andExpect(status().isOk())
88
+ .andExpect(jsonPath("$", hasSize(1)));
89
+
90
+ mockMvc
91
+ .perform(forNotes(get("/notes/" + id)))
92
+ .andExpect(status().isOk())
93
+ .andExpect(jsonPath("$.title", is("Hello")));
94
+
95
+ mockMvc
96
+ .perform(
97
+ forNotes(patch("/notes/" + id))
98
+ .contentType(MediaType.APPLICATION_JSON)
99
+ .content("{\"title\":\"Hi\"}"))
100
+ .andExpect(status().isOk())
101
+ .andExpect(jsonPath("$.title", is("Hi")))
102
+ .andExpect(jsonPath("$.body", is("World")));
103
+ }
104
+
105
+ @Test
106
+ void missingNoteIs404() throws Exception {
107
+ mockMvc.perform(forNotes(get("/notes/999"))).andExpect(status().isNotFound());
108
+ }
109
+
110
+ @Test
111
+ void blankTitleIsRejected() throws Exception {
112
+ mockMvc
113
+ .perform(
114
+ forNotes(post("/notes"))
115
+ .contentType(MediaType.APPLICATION_JSON)
116
+ .content("{\"title\":\" \",\"body\":\"x\"}"))
117
+ .andExpect(status().isBadRequest());
118
+ }
119
+ }
@@ -0,0 +1,3 @@
1
+ ## Database
2
+
3
+ Postgres through Spring Data JPA. Set `DATABASE_URL` (see `.env.example`). Tests use `ddl-auto=create-drop` against a throwaway database; apply real migrations yourself when you add them — the Yosr Seed gate does not run Flyway or Liquibase.
@@ -0,0 +1,5 @@
1
+ target/
2
+ .idea/
3
+ .git/
4
+ .env
5
+ *.iml
@@ -0,0 +1,20 @@
1
+ # Build
2
+ FROM eclipse-temurin:25-jdk AS build
3
+ RUN apt-get update \
4
+ && apt-get install -y --no-install-recommends maven \
5
+ && rm -rf /var/lib/apt/lists/*
6
+ WORKDIR /src
7
+ COPY pom.xml dependencies-lock.json ./
8
+ RUN mvn -B -q dependency:resolve dependency-lock:check
9
+ COPY src ./src
10
+ RUN mvn -B -q package -DskipTests
11
+
12
+ # Runtime
13
+ FROM eclipse-temurin:25-jre AS final
14
+ WORKDIR /app
15
+ RUN groupadd --system app && useradd --system --gid app --create-home app
16
+ COPY --from=build /src/target/app.jar ./app.jar
17
+ USER app
18
+ ENV JAVA_TOOL_OPTIONS=
19
+ EXPOSE 8080
20
+ ENTRYPOINT ["java", "-jar", "app.jar"]
@@ -0,0 +1,12 @@
1
+ # @seed:anchor compose-header
2
+ services:
3
+ app:
4
+ build: .
5
+ ports:
6
+ - "8080:8080"
7
+ environment:
8
+ APP_NAME: __seed_projectName__
9
+ # @seed:anchor app-env
10
+ # @seed:anchor app-config
11
+ # @seed:anchor services
12
+ # @seed:anchor compose-volumes
@@ -0,0 +1,3 @@
1
+ ## Docker
2
+
3
+ Multi-stage image: `eclipse-temurin:25-jdk` build → `eclipse-temurin:25-jre`, non-root user. `docker compose up --build` starts the API on port 8080.
@@ -0,0 +1,3 @@
1
+ depends_on:
2
+ db:
3
+ condition: service_healthy
@@ -0,0 +1 @@
1
+ DATABASE_URL: postgresql://postgres:postgres@db:5432/postgres
@@ -0,0 +1 @@
1
+ name: __seed_projectName__
@@ -0,0 +1,13 @@
1
+ db:
2
+ image: postgres:18.6-alpine
3
+ environment:
4
+ POSTGRES_PASSWORD: postgres
5
+ ports:
6
+ - "5432:5432"
7
+ volumes:
8
+ - pgdata:/var/lib/postgresql/data
9
+ healthcheck:
10
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
11
+ interval: 5s
12
+ timeout: 5s
13
+ retries: 10
@@ -0,0 +1,3 @@
1
+ ## Compose + Postgres
2
+
3
+ When `database=postgres`, Compose starts Postgres 18 and sets `DATABASE_URL` for the app service.
@@ -0,0 +1,64 @@
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 com.yosr.app.shared.Result;
7
+ import java.net.URI;
8
+ import java.util.List;
9
+ import java.util.Map;
10
+ import org.springframework.http.ResponseEntity;
11
+ import org.springframework.web.bind.annotation.GetMapping;
12
+ import org.springframework.web.bind.annotation.PatchMapping;
13
+ import org.springframework.web.bind.annotation.PathVariable;
14
+ import org.springframework.web.bind.annotation.PostMapping;
15
+ import org.springframework.web.bind.annotation.RequestBody;
16
+ import org.springframework.web.bind.annotation.RequestMapping;
17
+ import org.springframework.web.bind.annotation.RestController;
18
+
19
+ @RestController
20
+ @RequestMapping("/notes")
21
+ public class NotesController {
22
+ private final NotesService notes;
23
+
24
+ public NotesController(NotesService notes) {
25
+ this.notes = notes;
26
+ }
27
+
28
+ // @seed:anchor notes-group
29
+ @GetMapping
30
+ public List<NoteResponse> list() {
31
+ return notes.list().stream().map(NoteResponse::from).toList();
32
+ }
33
+
34
+ @GetMapping("/{id}")
35
+ public ResponseEntity<?> get(@PathVariable int id) {
36
+ return switch (notes.get(id)) {
37
+ case Result.Ok(Note note) -> ResponseEntity.ok(NoteResponse.from(note));
38
+ case Result.Err(String message) -> ResponseEntity.status(404).body(errorBody(message));
39
+ };
40
+ }
41
+
42
+ @PostMapping
43
+ public ResponseEntity<?> create(@RequestBody NoteCreate body) {
44
+ return switch (notes.create(body)) {
45
+ case Result.Ok(Note note) ->
46
+ ResponseEntity.created(URI.create("/notes/" + note.id())).body(NoteResponse.from(note));
47
+ case Result.Err(String message) -> ResponseEntity.badRequest().body(errorBody(message));
48
+ };
49
+ }
50
+
51
+ @PatchMapping("/{id}")
52
+ public ResponseEntity<?> update(@PathVariable int id, @RequestBody NoteUpdate body) {
53
+ return switch (notes.update(id, body)) {
54
+ case Result.Ok(Note note) -> ResponseEntity.ok(NoteResponse.from(note));
55
+ case Result.Err(String message) when message.toLowerCase().contains("not found") ->
56
+ ResponseEntity.status(404).body(errorBody(message));
57
+ case Result.Err(String message) -> ResponseEntity.badRequest().body(errorBody(message));
58
+ };
59
+ }
60
+
61
+ private static Map<String, String> errorBody(String detail) {
62
+ return Map.of("detail", detail);
63
+ }
64
+ }