@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,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,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,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,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 @@
|
|
|
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
|
package/features/errors-result/files/src/main/java/com/yosr/app/features/notes/NotesController.java
ADDED
|
@@ -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
|
+
}
|