diff --git a/google-cloud-sql/docs/cloud-sql-auth-proxy.md b/google-cloud-sql/docs/cloud-sql-auth-proxy.md
index 900717b..e3c6dee 100644
--- a/google-cloud-sql/docs/cloud-sql-auth-proxy.md
+++ b/google-cloud-sql/docs/cloud-sql-auth-proxy.md
@@ -1,11 +1,24 @@
# Cloud SQL Auth Proxy
-### Set instance connection name
+### Set environment variables
```bash
+export CREDENTIALS_PATH="/Users/admin/.config/gcloud/sa-private-key.json"
+export GOOGLE_APPLICATION_CREDENTIALS="$CREDENTIALS_PATH"
+export TF_VAR_project_id=$(gcloud config get-value project)
+export TF_VAR_region="us-central1"
+export TF_VAR_authorized_cidr="$(curl -4 -s ifconfig.me)/32"
+export TF_VAR_db_user=username
+export TF_VAR_db_password=password
+export TF_VAR_db_name=jm_demo_db
export INSTANCE_CONNECTION_NAME="lofty-root-378503:us-central1:jm-pg-demo"
+export PGPASSWORD="$TF_VAR_db_password"
```
+### Provision DB infrastructure
+
+Use Terraform or `gcloud` to create the Cloud SQL Postgres instance.
+
### Run the CloudSQL auth proxy
Remove any old proxy container:
@@ -28,8 +41,6 @@ PROXY_PID=$!
#### Docker
```bash
-
-export CREDENTIALS_PATH="/Users/admin/.config/gcloud/sa-private-key.json"
docker run -d --name csql-proxy -p 127.0.0.1:5400:5432 \
-v "$GOOGLE_APPLICATION_CREDENTIALS:/creds/key.json:ro" \
gcr.io/cloud-sql-connectors/cloud-sql-proxy:2 \
@@ -89,12 +100,6 @@ for p in {5400..5450}; do (lsof -iTCP:$p -sTCP:LISTEN >/dev/null) || { echo "Fir
### Test connection
-```bash
-export TF_VAR_db_user=username
-export TF_VAR_db_password=password
-export TF_VAR_db_name=jm_demo_db
-```
-
Have the proxy running and healthy to `127.0.0.1:5432`.
Make sure nothing else is using port `5432`.
If it is, pick another port (e.g., 5433) and update your `psql` command.
@@ -110,7 +115,7 @@ PGPASSWORD="$TF_VAR_db_password" psql \
### If the test is successful
-Assuming the file `/src/test/resources/init.sql` exists:
+Create the table from file contents. For example, assuming `/src/test/resources/init.sql` exists:
```bash
PGPASSWORD="$TF_VAR_db_password" psql \
@@ -205,6 +210,10 @@ DROP TABLE widgets;
### Exit `psql`
+```psql
+\q
+```
+
```psql
exit
```
diff --git a/google-cloud-sql/pom.xml b/google-cloud-sql/pom.xml
index 33e8ded..ad68e25 100644
--- a/google-cloud-sql/pom.xml
+++ b/google-cloud-sql/pom.xml
@@ -60,6 +60,14 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
org.springframework.cloud
@@ -78,6 +86,18 @@
true
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+
+
+ org.hibernate.orm
+ hibernate-core
+
+
org.projectlombok
lombok
@@ -91,6 +111,11 @@
true
+
+ com.h2database
+ h2
+ test
+
org.springframework.boot
spring-boot-starter-test
@@ -110,6 +135,12 @@
+
+ org.postgresql
+ postgresql
+ 42.7.7
+ runtime
+
com.google.cloud
spring-cloud-gcp-dependencies
diff --git a/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/controller/CloudSqlController.java b/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/controller/CloudSqlController.java
deleted file mode 100644
index 53facd6..0000000
--- a/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/controller/CloudSqlController.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.squidmin.java.spring.maven.cloudsql.controller;
-
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/cloud-sql")
-public class CloudSqlController {
-
- public CloudSqlController() {}
-
- @GetMapping(
- value = "/insert-rows",
- consumes = MediaType.APPLICATION_JSON_VALUE,
- produces = MediaType.APPLICATION_JSON_VALUE
- )
- public ResponseEntity query() {
- return ResponseEntity.ok("Placeholder response from Cloud SQL Controller");
- }
-
-}
diff --git a/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/controller/WidgetController.java b/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/controller/WidgetController.java
new file mode 100644
index 0000000..375054a
--- /dev/null
+++ b/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/controller/WidgetController.java
@@ -0,0 +1,54 @@
+package org.squidmin.java.spring.maven.cloudsql.controller;
+
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.squidmin.java.spring.maven.cloudsql.service.WidgetService;
+import org.squidmin.java.spring.maven.cloudsql.domain.Widget;
+
+import java.util.List;
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/cloud-sql/api/widgets")
+public class WidgetController {
+
+ private final WidgetService widgetService;
+
+ public WidgetController(WidgetService widgetService) {
+ this.widgetService = widgetService;
+ }
+
+ @GetMapping
+ public List list(@RequestParam(name = "q", required = false) String q) {
+ return widgetService.searchByName(q);
+ }
+
+ /**
+ * Get a widget by ID
+ * @param id the widget ID
+ * @return the widget if found, or 404 if not found
+ */
+ @GetMapping("{id}")
+ public ResponseEntity get(@PathVariable UUID id) {
+ return widgetService.findById(id)
+ .map(ResponseEntity::ok)
+ .orElse(ResponseEntity.notFound().build());
+ }
+
+ @PostMapping(
+ value = "/insert",
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ public ResponseEntity insert(@RequestBody Widget widget) {
+ // Generate ID server-side if not provided
+ if (widget.getId() == null) {
+ widget.setId(UUID.randomUUID());
+ }
+
+ Widget saved = widgetService.save(widget);
+ return ResponseEntity.ok(saved);
+ }
+
+}
diff --git a/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/domain/Widget.java b/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/domain/Widget.java
new file mode 100644
index 0000000..400873f
--- /dev/null
+++ b/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/domain/Widget.java
@@ -0,0 +1,46 @@
+package org.squidmin.java.spring.maven.cloudsql.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import org.hibernate.annotations.JdbcTypeCode;
+import org.hibernate.type.SqlTypes;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.UUID;
+
+@Entity
+@Table(name = "widgets")
+public class Widget {
+
+ @Id
+ @Column(name = "id", nullable = false, columnDefinition = "uuid")
+ private UUID id;
+
+ @Column(name = "name", nullable = false)
+ private String name;
+
+ // DB sets default now(); mark read-only so we don’t overwrite it
+ @Column(name = "created_at", columnDefinition = "timestamptz", updatable = false, insertable = false)
+ private Instant createdAt;
+
+ @JdbcTypeCode(SqlTypes.JSON)
+ @Column(name = "meta", columnDefinition = "jsonb")
+ private Map meta;
+
+ public Widget() {}
+
+ public UUID getId() { return id; }
+ public void setId(UUID id) { this.id = id; }
+
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+
+ public Instant getCreatedAt() { return createdAt; }
+
+ public Map getMeta() { return meta; }
+ public void setMeta(Map meta) { this.meta = meta; }
+
+}
diff --git a/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/repository/WidgetRepository.java b/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/repository/WidgetRepository.java
new file mode 100644
index 0000000..03d5d5f
--- /dev/null
+++ b/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/repository/WidgetRepository.java
@@ -0,0 +1,11 @@
+package org.squidmin.java.spring.maven.cloudsql.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.squidmin.java.spring.maven.cloudsql.domain.Widget;
+
+import java.util.List;
+import java.util.UUID;
+
+public interface WidgetRepository extends JpaRepository {
+ List findByNameContainingIgnoreCase(String q);
+}
diff --git a/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/service/WidgetService.java b/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/service/WidgetService.java
new file mode 100644
index 0000000..1a5362e
--- /dev/null
+++ b/google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/service/WidgetService.java
@@ -0,0 +1,36 @@
+package org.squidmin.java.spring.maven.cloudsql.service;
+
+import org.springframework.stereotype.Service;
+import org.squidmin.java.spring.maven.cloudsql.domain.Widget;
+import org.squidmin.java.spring.maven.cloudsql.repository.WidgetRepository;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@Service
+public class WidgetService {
+
+ private final WidgetRepository repo;
+
+ public WidgetService(WidgetRepository repo) {
+ this.repo = repo;
+ }
+
+ public List findAll() {
+ return repo.findAll();
+ }
+
+ public Optional findById(UUID id) {
+ return repo.findById(id);
+ }
+
+ public List searchByName(String q) {
+ return (q == null || q.isBlank()) ? repo.findAll() : repo.findByNameContainingIgnoreCase(q);
+ }
+
+ public Widget save(Widget widget) {
+ return repo.save(widget);
+ }
+
+}
diff --git a/google-cloud-sql/src/main/resources/application.yml b/google-cloud-sql/src/main/resources/application.yml
index ac29fc0..65f61bf 100644
--- a/google-cloud-sql/src/main/resources/application.yml
+++ b/google-cloud-sql/src/main/resources/application.yml
@@ -1,3 +1,18 @@
spring:
application:
name: google-cloud-sql
+ datasource:
+ url: jdbc:postgresql://127.0.0.1:5400/jm_demo_db
+ username: username
+ password: ${DB_PASSWORD:password}
+ driver-class-name: org.postgresql.Driver
+ hikari:
+ maximum-pool-size: 10
+ minimum-idle: 2
+
+ jpa:
+ open-in-view: false
+ hibernate:
+ ddl-auto: none
+ properties:
+ hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
diff --git a/google-cloud-sql/src/test/java/org/squidmin/java/spring/maven/cloudsql/actuator/ActuatorTest.java b/google-cloud-sql/src/test/java/org/squidmin/java/spring/maven/cloudsql/actuator/ActuatorTest.java
new file mode 100644
index 0000000..b7e0f30
--- /dev/null
+++ b/google-cloud-sql/src/test/java/org/squidmin/java/spring/maven/cloudsql/actuator/ActuatorTest.java
@@ -0,0 +1,45 @@
+package org.squidmin.java.spring.maven.cloudsql.actuator;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.http.ResponseEntity;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest(
+ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+ properties = {
+ "management.endpoints.web.exposure.include=health,info",
+ "management.endpoint.health.show-details=always",
+ "spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1",
+ "spring.datasource.driver-class-name=org.h2.Driver",
+ "spring.jpa.hibernate.ddl-auto=none"
+ }
+)
+public class ActuatorTest {
+
+ @LocalServerPort
+ int port;
+
+ @Autowired
+ TestRestTemplate rest;
+
+ @Test
+ void health_isUp() {
+ ResponseEntity res =
+ rest.getForEntity("http://localhost:" + port + "/actuator/health", String.class);
+ assertThat(res.getStatusCode().is2xxSuccessful()).isTrue();
+ assertThat(res.getBody()).contains("\"status\":\"UP\"");
+ }
+
+ @Test
+ void info_isReachable() {
+ ResponseEntity res =
+ rest.getForEntity("http://localhost:" + port + "/actuator/info", String.class);
+ assertThat(res.getStatusCode().is2xxSuccessful()).isTrue();
+ }
+
+}
diff --git a/google-cloud-sql/src/test/resources/application-test.yml b/google-cloud-sql/src/test/resources/application-test.yml
index ac29fc0..65f61bf 100644
--- a/google-cloud-sql/src/test/resources/application-test.yml
+++ b/google-cloud-sql/src/test/resources/application-test.yml
@@ -1,3 +1,18 @@
spring:
application:
name: google-cloud-sql
+ datasource:
+ url: jdbc:postgresql://127.0.0.1:5400/jm_demo_db
+ username: username
+ password: ${DB_PASSWORD:password}
+ driver-class-name: org.postgresql.Driver
+ hikari:
+ maximum-pool-size: 10
+ minimum-idle: 2
+
+ jpa:
+ open-in-view: false
+ hibernate:
+ ddl-auto: none
+ properties:
+ hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
diff --git a/google-cloud-sql/src/test/resources/init.sql b/google-cloud-sql/src/test/resources/init.sql
index 664bd59..c9927bb 100644
--- a/google-cloud-sql/src/test/resources/init.sql
+++ b/google-cloud-sql/src/test/resources/init.sql
@@ -1,6 +1,7 @@
-CREATE TABLE IF NOT EXISTS widgets (
- id UUID PRIMARY KEY,
- name TEXT NOT NULL,
- created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
- meta JSONB
+CREATE TABLE IF NOT EXISTS widgets
+(
+ id UUID PRIMARY KEY,
+ name TEXT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ meta JSONB
);
diff --git a/java21-spring3-maven-reference/cloud-sql-api-widgets-{id}.bru b/java21-spring3-maven-reference/cloud-sql-api-widgets-{id}.bru
new file mode 100644
index 0000000..3cfe14d
--- /dev/null
+++ b/java21-spring3-maven-reference/cloud-sql-api-widgets-{id}.bru
@@ -0,0 +1,15 @@
+meta {
+ name: /cloud-sql/api/widgets/{id}
+ type: http
+ seq: 15
+}
+
+get {
+ url: http://localhost:8080/cloud-sql/api/widgets/ca0fbbdc-4ccd-4a98-9b13-08565e9db5a0
+ body: none
+ auth: inherit
+}
+
+settings {
+ encodeUrl: true
+}
diff --git a/java21-spring3-maven-reference/cloud-sql-api-widgets.bru b/java21-spring3-maven-reference/cloud-sql-api-widgets.bru
new file mode 100644
index 0000000..7cfdeb4
--- /dev/null
+++ b/java21-spring3-maven-reference/cloud-sql-api-widgets.bru
@@ -0,0 +1,19 @@
+meta {
+ name: /cloud-sql/api/widgets
+ type: http
+ seq: 13
+}
+
+get {
+ url: http://localhost:8080/cloud-sql/api/widgets?q=Test Widget
+ body: none
+ auth: inherit
+}
+
+params:query {
+ q: Test Widget
+}
+
+settings {
+ encodeUrl: true
+}