Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties

### Local secrets ###
application-local.yaml

### Project ###
contacts.zip
contacts.json
Expand Down
1 change: 1 addition & 0 deletions .idea/dictionaries/project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions .run/Docker Compose Up.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docker Compose Up" type="ShConfigurationType" factoryName="Shell Script">
<option name="SCRIPT_TEXT" value="docker compose up -d" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
</component>
3 changes: 2 additions & 1 deletion .run/Spring Development.run.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Spring Development" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
<option name="ACTIVE_PROFILES" value="dev" />
<option name="ACTIVE_PROFILES" value="local" />
<module name="contacts" />
<option name="SPRING_BOOT_MAIN_CLASS" value="codes.yam.contacts.ContactsApplication" />
<method v="2">
<option name="Make" enabled="true" />
<option name="ToolBeforeRunTask" enabled="true" actionId="Tool_External Tools_Docker Compose Up + Wait" />
</method>
</configuration>
</component>
110 changes: 78 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,53 +21,90 @@ This app follows the architecture from [Hypermedia Systems](https://hypermedia.s
- Add, edit, and delete contact details
- Server-rendered HTML with Thymeleaf
- Partial page updates (no full reloads)
- H2 in-memory database with Spring Data JPA
- PostgreSQL + Spring Data JPA + Flyway migrations

## Development

### Quickstart
### Prerequisites

- Java 25
- Docker (for local Postgres)

### Setup

1. **Create `./config/application-local.yaml`** (gitignored — never committed, lives outside the source tree):

```yaml
spring:
security:
user:
name: <your-username>
password: <your-password>
datasource:
url: jdbc:postgresql://localhost:5432/contacts
username: contacts
password: contacts
jpa:
hibernate:
ddl-auto: validate
sql:
init:
mode: always
thymeleaf:
cache: false
prefix: file:src/main/resources/templates/
devtools:
livereload:
enabled: true
restart:
additional-paths: src/main/resources/templates/
additional-exclude: "**/*.html"
web:
resources:
static-locations:
- file:src/main/resources/static/
- classpath:/static/
cache:
period: 0
chain:
cache: false
```

```bash
# Start dev server
./mvnw spring-boot:run
2. **Start Postgres:**

# Open in browser
open http://localhost:8080
```bash
./scripts/start-db.sh
```

### VS Code
3. **Run the app:**

1. **Install extensions** - open the Extensions panel, search `@recommended`, and install all workspace recommendations (Java Extension Pack, Spring Boot Extension Pack, Lombok, htmx attributes)
2. **Java 25 SDK** - VS Code will prompt to download a JDK if none is found; select Java 25 (if you have multiple JDKs, point `java.jdt.ls.java.home` in user settings to your Java 25 installation)
3. **Run** - use the **Run** task via `Ctrl+Shift+P → Tasks: Run Task → Run`, or press `Ctrl+Shift+P → Spring Boot Dashboard: Run` from the Spring Boot extension

**Tips:**

- Spring Boot DevTools (already included) enables hot reload - save a file and changes apply automatically
- Thymeleaf templates hot-reload without a restart
- H2 console is available at `http://localhost:8080/h2-console`
```bash
./mvnw spring-boot:run -Dspring-boot.run.profiles=local
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### IntelliJ IDEA

1. **Open** - **File → Open**, select `pom.xml`, choose **"Open as Project"**
2. **Java 25 SDK** - **File → Project Structure → SDKs**, add Java 25 if missing (IntelliJ can download it)
3. **Lombok** - install the [Lombok plugin](https://plugins.jetbrains.com/plugin/6317-lombok) via **Settings → Plugins**, then enable annotation processing under **Settings → Build, Execution, Deployment → Compiler → Annotation Processors**
4. **Run** - use the included **Contacts** run configuration (`.run/Contacts.run.xml`) from the toolbar, or run `spring-boot:run` from the Maven tool window
4. **Create `./config/application-local.yaml`** - see [Setup](#setup) above
5. **Run** - use the included **Development** compound run configuration from the toolbar — it starts Docker, waits for Postgres, then launches the app

**Tips:**

- Spring Boot DevTools (already included) enables hot reload - recompile with `Ctrl+F9` without restarting
- Enable **"Build project automatically"** (**Settings → Build, Execution, Deployment → Compiler**) and **"Allow auto-make to start even if developed application is currently running"** (**Settings → Advanced Settings**) for seamless reloads on save
- Thymeleaf templates hot-reload without a restart
- H2 console is available at `http://localhost:8080/h2-console`

### Scripts

| Command | Description |
| ------------------------ | ---------------- |
| `./mvnw spring-boot:run` | Start dev server |
| `./mvnw test` | Run tests |
| `./mvnw package` | Build jar |
| Command | Description |
| --------------------------------------------------------- | -------------------- |
| `./scripts/start-db.sh` | Start local Postgres |
| `docker compose down` | Stop local Postgres |
| `./mvnw spring-boot:run -Dspring-boot.run.profiles=local` | Start dev server |
| `./mvnw test` | Run tests |
| `./mvnw package` | Build jar |

## Project structure

Expand All @@ -81,6 +118,9 @@ src/
│ │ ├── ContactRepository.java # Spring Data JPA (search, pagination)
│ │ └── ContactService.java # Validation, slug generation, business logic
│ └── resources/
│ ├── db/migration/
│ │ ├── V1__init.sql # Initial schema (Flyway)
│ │ └── V2__seed.sql # Seed data (Flyway)
│ ├── templates/
│ │ ├── layout.html # Base layout (Thymeleaf fragment)
│ │ ├── contacts/
Expand All @@ -97,13 +137,18 @@ src/
│ │ ├── styles.css
│ │ └── img/
│ │ └── spinning-circles.svg # Loading spinner
│ └── application.yaml
└── test/
└── java/codes/yam/contacts/
├── controller/
│ └── ContactControllerTest.java
└── service/
└── ContactServiceTest.java
│ └── application.yaml # Production config (env var placeholders)
config/
└── application-local.yaml # Local overrides — gitignored, never committed
├── test/
│ └── java/codes/yam/contacts/
│ ├── controller/
│ │ └── ContactControllerTest.java
│ └── service/
│ └── ContactServiceTest.java
scripts/
└── start-db.sh # docker compose up + wait for Postgres readiness
docker-compose.yml # Local Postgres service
```

### Routes
Expand All @@ -130,7 +175,8 @@ src/
| Framework | [Spring Boot](https://spring.io/projects/spring-boot) 4.0.3 |
| Templating | [Thymeleaf](https://www.thymeleaf.org) |
| Hypermedia | [htmx](https://htmx.org) + [htmx-spring-boot-thymeleaf](https://github.com/wimdeblauwe/htmx-spring-boot) |
| Database | H2 (in-memory) + Spring Data JPA |
| Database | PostgreSQL + Spring Data JPA |
| Migrations | [Flyway](https://flywaydb.org) |
| Build | Maven |

## Branches
Expand Down
19 changes: 19 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
services:
db:
image: postgres:17
environment:
POSTGRES_DB: contacts
POSTGRES_USER: contacts
POSTGRES_PASSWORD: contacts
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U contacts -d contacts"]
interval: 5s
timeout: 5s
retries: 10

volumes:
postgres_data:
19 changes: 16 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,19 @@
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-h2console</artifactId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<artifactId>spring-boot-starter-flyway</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down Expand Up @@ -72,10 +80,15 @@
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
Expand Down
12 changes: 12 additions & 0 deletions scripts/start-db.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh
MAX_RETRIES=30
docker compose up -d
i=0
until docker compose exec -T db pg_isready -q; do
i=$((i + 1))
if [ "$i" -ge "$MAX_RETRIES" ]; then
echo "Postgres did not become ready after $MAX_RETRIES seconds" >&2
exit 1
fi
sleep 1
done
Comment thread
coderabbitai[bot] marked this conversation as resolved.
8 changes: 2 additions & 6 deletions src/main/java/codes/yam/contacts/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,16 @@
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) {
return http.authorizeHttpRequests(
auth -> auth.requestMatchers("/h2-console/**").permitAll().anyRequest().authenticated())
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**"))
.build();
}
}
26 changes: 0 additions & 26 deletions src/main/resources/application-dev.yaml

This file was deleted.

8 changes: 6 additions & 2 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ spring:
thymeleaf:
cache: true
jpa:
defer-datasource-initialization: true
open-in-view: false
hibernate:
ddl-auto: validate
datasource:
url: jdbc:h2:mem:contacts;DB_CLOSE_DELAY=-1
url: ${DATABASE_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
security:
user:
name: ${APP_USERNAME}
Expand Down
8 changes: 8 additions & 0 deletions src/main/resources/db/migration/V1__init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE contact (
id UUID PRIMARY KEY,
slug VARCHAR(255),
first VARCHAR(255),
last VARCHAR(255),
email VARCHAR(255),
phone VARCHAR(255)
);
31 changes: 31 additions & 0 deletions src/main/resources/db/migration/V2__seed.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
INSERT INTO contact (id, slug, first, last, email, phone) VALUES
('550e8400-e29b-41d4-a716-446655440000', 'john-doe', 'John', 'Doe', 'john@example.com', '555-1234'),
('550e8400-e29b-41d4-a716-446655440001', 'jane-doe', 'Jane', 'Doe', 'jane@example.com', '555-5678'),
('550e8400-e29b-41d4-a716-446655440002', 'alice-smith', 'Alice', 'Smith', 'alice.smith@example.com', '555-1001'),
('550e8400-e29b-41d4-a716-446655440003', 'bob-johnson', 'Bob', 'Johnson', 'bob.johnson@example.com', '555-1002'),
('550e8400-e29b-41d4-a716-446655440004', 'carol-williams', 'Carol', 'Williams', 'carol.williams@example.com', '555-1003'),
('550e8400-e29b-41d4-a716-446655440005', 'david-brown', 'David', 'Brown', 'david.brown@example.com', '555-1004'),
('550e8400-e29b-41d4-a716-446655440006', 'eve-jones', 'Eve', 'Jones', 'eve.jones@example.com', '555-1005'),
('550e8400-e29b-41d4-a716-446655440007', 'frank-garcia', 'Frank', 'Garcia', 'frank.garcia@example.com', '555-1006'),
('550e8400-e29b-41d4-a716-446655440008', 'grace-miller', 'Grace', 'Miller', 'grace.miller@example.com', '555-1007'),
('550e8400-e29b-41d4-a716-446655440009', 'henry-davis', 'Henry', 'Davis', 'henry.davis@example.com', '555-1008'),
('550e8400-e29b-41d4-a716-44665544000a', 'iris-martinez', 'Iris', 'Martinez', 'iris.martinez@example.com', '555-1009'),
('550e8400-e29b-41d4-a716-44665544000b', 'jack-wilson', 'Jack', 'Wilson', 'jack.wilson@example.com', '555-1010'),
('550e8400-e29b-41d4-a716-44665544000c', 'karen-anderson', 'Karen', 'Anderson', 'karen.anderson@example.com', '555-1011'),
('550e8400-e29b-41d4-a716-44665544000d', 'leo-thomas', 'Leo', 'Thomas', 'leo.thomas@example.com', '555-1012'),
('550e8400-e29b-41d4-a716-44665544000e', 'mia-taylor', 'Mia', 'Taylor', 'mia.taylor@example.com', '555-1013'),
('550e8400-e29b-41d4-a716-44665544000f', 'noah-hernandez', 'Noah', 'Hernandez', 'noah.hernandez@example.com', '555-1014'),
('550e8400-e29b-41d4-a716-446655440010', 'olivia-moore', 'Olivia', 'Moore', 'olivia.moore@example.com', '555-1015'),
('550e8400-e29b-41d4-a716-446655440011', 'paul-jackson', 'Paul', 'Jackson', 'paul.jackson@example.com', '555-1016'),
('550e8400-e29b-41d4-a716-446655440012', 'quinn-martin', 'Quinn', 'Martin', 'quinn.martin@example.com', '555-1017'),
('550e8400-e29b-41d4-a716-446655440013', 'rachel-lee', 'Rachel', 'Lee', 'rachel.lee@example.com', '555-1018'),
('550e8400-e29b-41d4-a716-446655440014', 'sam-perez', 'Sam', 'Perez', 'sam.perez@example.com', '555-1019'),
('550e8400-e29b-41d4-a716-446655440015', 'tina-white', 'Tina', 'White', 'tina.white@example.com', '555-1020'),
('550e8400-e29b-41d4-a716-446655440016', 'ursula-harris', 'Ursula', 'Harris', 'ursula.harris@example.com', '555-1021'),
('550e8400-e29b-41d4-a716-446655440017', 'victor-clark', 'Victor', 'Clark', 'victor.clark@example.com', '555-1022'),
('550e8400-e29b-41d4-a716-446655440018', 'wendy-lewis', 'Wendy', 'Lewis', 'wendy.lewis@example.com', '555-1023'),
('550e8400-e29b-41d4-a716-446655440019', 'xavier-robinson', 'Xavier', 'Robinson', 'xavier.robinson@example.com', '555-1024'),
('550e8400-e29b-41d4-a716-44665544001a', 'yara-walker', 'Yara', 'Walker', 'yara.walker@example.com', '555-1025'),
('550e8400-e29b-41d4-a716-44665544001b', 'zane-hall', 'Zane', 'Hall', 'zane.hall@example.com', '555-1026'),
('550e8400-e29b-41d4-a716-44665544001c', 'amber-allen', 'Amber', 'Allen', 'amber.allen@example.com', '555-1027'),
('550e8400-e29b-41d4-a716-44665544001d', 'brian-young', 'Brian', 'Young', 'brian.young@example.com', '555-1028');
Comment on lines +1 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Seed migration won’t execute from db/seed with current Flyway config.

With no spring.flyway.locations configured in src/main/resources/application.yaml:1-18, Flyway scans only classpath:db/migration. This file is in classpath:db/seed, so these inserts are skipped and seed data won’t load.

Use one of these fixes:

  • Move this file to src/main/resources/db/migration/V2__seed.sql, or
  • Configure spring.flyway.locations: classpath:db/migration,classpath:db/seed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/db/seed/V2__seed.sql` around lines 1 - 31, The Flyway seed
file V2__seed.sql is placed under db/seed but your app's Flyway config
(spring.flyway.locations in application.yaml) defaults to classpath:db/migration
so the seed never runs; fix by either moving V2__seed.sql into db/migration
(e.g., src/main/resources/db/migration/V2__seed.sql) or updating
application.yaml to set spring.flyway.locations to include both
classpath:db/migration and classpath:db/seed so Flyway will scan the seed
directory.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== data.sql (if present) =="
if [ -f src/main/resources/data.sql ]; then
  sed -n '1,220p' src/main/resources/data.sql
else
  echo "src/main/resources/data.sql not found"
fi

echo
echo "== Flyway/SQL init related config =="
rg -n -C2 'flyway|sql:|init:|defer-datasource-initialization|datasource' \
  src/main/resources/application.yaml src/test/resources/application.yaml 2>/dev/null || true

Repository: yamcodes/contacts

Length of output: 4659


🏁 Script executed:

cat -n src/main/resources/application.yaml

Repository: yamcodes/contacts

Length of output: 494


🏁 Script executed:

# Check for any other Flyway-related configurations
fd -e yaml -e properties | xargs rg -l 'flyway' 2>/dev/null || true

Repository: yamcodes/contacts

Length of output: 96


🏁 Script executed:

# Check if there's a bootstrap.properties or other config files
fd -e properties -e yml -o -e yaml | head -20

Repository: yamcodes/contacts

Length of output: 177


🏁 Script executed:

fd 'application.*\.(yaml|yml|properties)' src/main/resources/ src/test/resources/

Repository: yamcodes/contacts

Length of output: 132


🏁 Script executed:

# Check if there's a pom.xml or build.gradle to understand profiles
find . -maxdepth 2 -name 'pom.xml' -o -name 'build.gradle' | xargs ls -la 2>/dev/null || echo "No pom.xml or build.gradle found in root"

Repository: yamcodes/contacts

Length of output: 119


🏁 Script executed:

# Check if sql.init.mode is configured anywhere in the codebase
rg 'sql\.init\.mode|sql:' src/ --type yaml --type properties 2>/dev/null || echo "Not found"

Repository: yamcodes/contacts

Length of output: 70


🏁 Script executed:

rg -A5 -B2 'flyway' pom.xml

Repository: yamcodes/contacts

Length of output: 668


🏁 Script executed:

# List all SQL migration files
fd -e sql src/main/resources/db/

Repository: yamcodes/contacts

Length of output: 454


🏁 Script executed:

# Check if there's a V1 migration
cat -n src/main/resources/db/migration/V1__*.sql 2>/dev/null || echo "No V1 migration found"

Repository: yamcodes/contacts

Length of output: 309


🏁 Script executed:

fd . src/main/resources/db/

Repository: yamcodes/contacts

Length of output: 206


🏁 Script executed:

# Look for any configuration about Flyway locations
rg 'flyway\.locations|spring\.flyway' src/main/resources/ src/test/resources/

Repository: yamcodes/contacts

Length of output: 43


Move V2__seed.sql to standard Flyway location and verify single seeding path after fix.

The seed migration V2__seed.sql is in src/main/resources/db/seed/ but Flyway searches db/migration/ by default. This means in production, neither the Flyway migration nor data.sql will seed the database (sql.init.mode defaults to NEVER for non-H2 databases). Once the migration is moved to db/migration/, confirm that data.sql is not also executed (it won't be, since sql.init.mode is not configured for production, but document this to prevent future confusion).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/db/seed/V2__seed.sql` around lines 1 - 31, Move the seed
migration file V2__seed.sql out of src/main/resources/db/seed/ into the
Flyway-managed directory db/migration/ so Flyway will pick it up (remove the old
copy in db/seed/ to avoid confusion), then run migrations (or a local prod-like
startup) to confirm the data is seeded by Flyway and not duplicated by any other
mechanism; specifically check for any data.sql file and verify sql.init.mode
remains at its production default so data.sql is not executed, and update
project notes/README to document that Flyway (db/migration/) is the single
canonical seeding path.

Loading
Loading