From db12114f0efc9c3cb1fbcaa4889ad9e34a223fea Mon Sep 17 00:00:00 2001 From: Shadi Date: Tue, 30 Jun 2026 17:16:01 +0200 Subject: [PATCH 1/4] Implement product details JSONB queries and size update - Include details field in product responses - Add color filtering using product details JSONB data - Implement findByColor in ProductRepository - Add JSONB size update query with jsonb_set - Fix PUT /products/{id}/size endpoint --- .../hyfshop/product/Product.java | 2 + .../hyfshop/product/ProductController.java | 12 ++-- .../hyfshop/product/ProductRepository.java | 55 ++++++++++++++++--- .../hyfshop/product/dto/ProductResponse.java | 7 ++- src/main/resources/application.yaml | 6 +- 5 files changed, 64 insertions(+), 18 deletions(-) diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/Product.java b/src/main/java/net/hackyourfuture/hyfshop/product/Product.java index ccc6204..a3108b9 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/Product.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/Product.java @@ -6,6 +6,7 @@ import lombok.Setter; import java.math.BigDecimal; +import java.util.Map; @AllArgsConstructor @NoArgsConstructor @@ -17,4 +18,5 @@ public class Product { private BigDecimal price; private String category; private String imageUrl; + private Map details; } diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java index 9e6c162..9f8f13a 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java @@ -21,16 +21,20 @@ public List getProducts() { } @GetMapping("/search") - public List searchProducts(@Nullable @RequestParam("color") String color) { - if (color == null) { + public List searchProducts(@RequestParam(value = "color", required = false) String color) { + if (color == null || color.isBlank()) { return productService.getAllProducts(); } + return productService.searchProducts(color); } @PutMapping("/{id}/size") - public ProductResponse setProductSize(@PathVariable int id, @RequestBody SetSizeRequest request) { - return productService.setProductSize(id, request.size()); + public ProductResponse setSize( + @PathVariable int id, + @RequestParam String size + ) { + return productService.setProductSize(id, size); } @PutMapping("/{id}/image") diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java index 2766734..41040f4 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java @@ -4,35 +4,56 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.stereotype.Repository; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; + import java.util.List; +import java.util.Map; @Repository @AllArgsConstructor public class ProductRepository { private final JdbcClient jdbcClient; + private static final ObjectMapper objectMapper = new ObjectMapper(); public static final RowMapper PRODUCT_ROW_MAPPER = (rs, _) -> { - var product = new Product(); + Product product = new Product(); product.setId(rs.getInt("id")); product.setTitle(rs.getString("title")); product.setPrice(rs.getBigDecimal("price")); product.setCategory(rs.getString("category")); product.setImageUrl(rs.getString("image_url")); + try { + String json = rs.getString("details"); + if (json != null) { + product.setDetails(objectMapper.readValue(json, + new TypeReference>() { + })); + } + } + catch (Exception e) { + throw new RuntimeException(e); + + } + return product; }; public List getAllProducts() { return jdbcClient - .sql("SELECT id, title, price, category, image_url FROM products") + .sql("SELECT id, title, price, category, image_url, details FROM products") .query(PRODUCT_ROW_MAPPER) .list(); } public Product findById(int id) { - return jdbcClient - .sql("SELECT id, title, price, category, image_url FROM products WHERE id = :id") - .param("id", id) + return jdbcClient.sql(""" + SELECT id, title, price, category, image_url, details + FROM products + WHERE id = ? + """) + .param(id) .query(PRODUCT_ROW_MAPPER) .single(); } @@ -49,12 +70,28 @@ public void setImageUrl(int id, String imageUrl) { } public List findByColor(String color) { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + return jdbcClient.sql(""" + SELECT * + FROM products + WHERE details->>'color' = ? + OR jsonb_exists(details->'colors', ?) + """) + .param(color) + .param(color) + .query(PRODUCT_ROW_MAPPER) + .list(); } public Product setSize(int id, String size) { - // TODO: Implement - throw new UnsupportedOperationException("Not implemented yet"); + return jdbcClient.sql(""" + UPDATE products + SET details = jsonb_set(details, '{size}', to_jsonb(CAST(? AS text))) + WHERE id = ? + RETURNING * + """) + .param(size) + .param(id) + .query(PRODUCT_ROW_MAPPER) + .single(); } } diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/dto/ProductResponse.java b/src/main/java/net/hackyourfuture/hyfshop/product/dto/ProductResponse.java index 0c35ef1..4206c4e 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/dto/ProductResponse.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/dto/ProductResponse.java @@ -3,13 +3,15 @@ import net.hackyourfuture.hyfshop.product.Product; import java.math.BigDecimal; +import java.util.Map; public record ProductResponse ( int id, String title, BigDecimal price, String category, - String imageUrl + String imageUrl , + Map details ){ public static ProductResponse from(Product product) { return new ProductResponse( @@ -17,7 +19,8 @@ public static ProductResponse from(Product product) { product.getTitle(), product.getPrice(), product.getCategory(), - product.getImageUrl() + product.getImageUrl(), + product.getDetails() ); } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 05b7788..bed19c0 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -2,8 +2,8 @@ spring: application: name: hyf_shop datasource: - url: '${DB_URL}' - username: '${DB_USERNAME}' - password: '${DB_PASSWORD}' + url: 'jdbc:postgresql://localhost:5432/hyfshop' + username: 'hyfuser' + password: 'hyfpassword' server: port: '8080' From 19ed595a2b821e80e65a6a3614a91074bfbb9e29 Mon Sep 17 00:00:00 2001 From: Shadi Date: Tue, 30 Jun 2026 19:57:20 +0200 Subject: [PATCH 2/4] Add product image upload support with Backblaze B2 - Configure S3-compatible Backblaze client - Add file upload and delete service - Save public image URLs in product image_url - Implement product image upload and delete endpoints --- pom.xml | 5 +++ .../hyfshop/product/ProductController.java | 10 ++---- .../hyfshop/product/ProductRepository.java | 35 +++++++++---------- .../hyfshop/product/ProductService.java | 31 ++++++++++++---- src/main/resources/application.yaml | 17 ++++++--- 5 files changed, 63 insertions(+), 35 deletions(-) diff --git a/pom.xml b/pom.xml index bbdda38..78d0f07 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,11 @@ org.springframework.boot spring-boot-starter-webclient + + software.amazon.awssdk + s3 + 2.25.0 + org.springframework.boot spring-boot-starter-webmvc diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java index 9f8f13a..345418b 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductController.java @@ -1,6 +1,5 @@ package net.hackyourfuture.hyfshop.product; -import jakarta.annotation.Nullable; import lombok.AllArgsConstructor; import net.hackyourfuture.hyfshop.product.dto.ProductResponse; import net.hackyourfuture.hyfshop.product.dto.SetSizeRequest; @@ -30,15 +29,12 @@ public List searchProducts(@RequestParam(value = "color", requi } @PutMapping("/{id}/size") - public ProductResponse setSize( - @PathVariable int id, - @RequestParam String size - ) { - return productService.setProductSize(id, size); + public ProductResponse setSize(@PathVariable int id, @RequestBody SetSizeRequest request) { + return productService.setProductSize(id, request.size()); } @PutMapping("/{id}/image") - public ProductResponse setProductImage(@PathVariable int id, @RequestBody MultipartFile file) { + public ProductResponse setProductImage(@PathVariable int id, @RequestParam("file") MultipartFile file) { return productService.setProductImage(id, file); } diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java index 41040f4..4d1713a 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java @@ -30,11 +30,10 @@ public class ProductRepository { new TypeReference>() { })); } - } - catch (Exception e) { - throw new RuntimeException(e); + } catch (Exception e) { + throw new RuntimeException(e); - } + } return product; }; @@ -49,10 +48,10 @@ public List getAllProducts() { public Product findById(int id) { return jdbcClient.sql(""" - SELECT id, title, price, category, image_url, details - FROM products - WHERE id = ? - """) + SELECT id, title, price, category, image_url, details + FROM products + WHERE id = ? + """) .param(id) .query(PRODUCT_ROW_MAPPER) .single(); @@ -71,11 +70,11 @@ public void setImageUrl(int id, String imageUrl) { public List findByColor(String color) { return jdbcClient.sql(""" - SELECT * - FROM products - WHERE details->>'color' = ? - OR jsonb_exists(details->'colors', ?) - """) + SELECT * + FROM products + WHERE details->>'color' = ? + OR jsonb_exists(details->'colors', ?) + """) .param(color) .param(color) .query(PRODUCT_ROW_MAPPER) @@ -84,11 +83,11 @@ OR jsonb_exists(details->'colors', ?) public Product setSize(int id, String size) { return jdbcClient.sql(""" - UPDATE products - SET details = jsonb_set(details, '{size}', to_jsonb(CAST(? AS text))) - WHERE id = ? - RETURNING * - """) + UPDATE products + SET details = jsonb_set(details, '{size}', to_jsonb(CAST(? AS text))) + WHERE id = ? + RETURNING * + """) .param(size) .param(id) .query(PRODUCT_ROW_MAPPER) diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductService.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductService.java index 779d3b2..2f562ec 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductService.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductService.java @@ -11,6 +11,7 @@ @RequiredArgsConstructor public class ProductService { private final ProductRepository productRepository; + private final FileService fileService; public List getAllProducts() { return productRepository.getAllProducts().stream().map(ProductResponse::from).toList(); @@ -26,14 +27,32 @@ public ProductResponse setProductSize(int id, String size) { } public ProductResponse setProductImage(int id, MultipartFile file) { - // TODO: Implement - // call ProductRepository.setImageUrl() afterwards with the new URL - throw new UnsupportedOperationException("Not implemented yet"); + try { + String imageKey = fileService.upload(file); + + productRepository.setImageUrl(id, imageKey); + + Product product = productRepository.findById(id); + + return ProductResponse.from(product); + } catch (Exception e) { + throw new RuntimeException("Could not upload product image", e); + } } public ProductResponse deleteProductImage(int id) { - // TODO: Implement - // call ProductRepository.setImageUrl() to set the image url to null - throw new UnsupportedOperationException("Not implemented yet"); + Product product = productRepository.findById(id); + + String imageUrl = product.getImageUrl(); + + if (imageUrl != null && !imageUrl.isBlank()) { + fileService.delete(imageUrl); + } + + productRepository.setImageUrl(id, null); + + Product updatedProduct = productRepository.findById(id); + + return ProductResponse.from(updatedProduct); } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index bed19c0..703fea0 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -2,8 +2,17 @@ spring: application: name: hyf_shop datasource: - url: 'jdbc:postgresql://localhost:5432/hyfshop' - username: 'hyfuser' - password: 'hyfpassword' + url: ${DB_URL:jdbc:postgresql://localhost:5432/hyfshop} + username: ${DB_USERNAME:hyfuser} + password: ${DB_PASSWORD:hyfpassword} + server: - port: '8080' + port: ${SERVER_PORT:8080} + +b2: + endpoint: ${B2_ENDPOINT:https://s3.us-east-005.backblazeb2.com} + region: ${B2_REGION:us-east-005} + access-key: ${B2_ACCESS_KEY} + secret-key: ${B2_SECRET_KEY} + bucket: ${B2_BUCKET:hyfshop-product-images-dev} + public-url: https://f005.backblazeb2.com/file/hyfshop-product-images-dev \ No newline at end of file From 8564d3975f12bb1575286e2d33911c79bb5bb83f Mon Sep 17 00:00:00 2001 From: Shadi Date: Tue, 30 Jun 2026 20:06:12 +0200 Subject: [PATCH 3/4] Add product image upload support with Backblaze B2 - Configure S3-compatible Backblaze client - Add file upload and delete service - Save public image URLs in product image_url - Implement product image upload and delete endpoints --- .../hyfshop/configuration/B2Config.java | 37 +++++++++++++ .../hyfshop/product/FileService.java | 52 +++++++++++++++++++ .../hyfshop/product/ProductRepository.java | 8 +-- 3 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 src/main/java/net/hackyourfuture/hyfshop/configuration/B2Config.java create mode 100644 src/main/java/net/hackyourfuture/hyfshop/product/FileService.java diff --git a/src/main/java/net/hackyourfuture/hyfshop/configuration/B2Config.java b/src/main/java/net/hackyourfuture/hyfshop/configuration/B2Config.java new file mode 100644 index 0000000..48c24b7 --- /dev/null +++ b/src/main/java/net/hackyourfuture/hyfshop/configuration/B2Config.java @@ -0,0 +1,37 @@ +package net.hackyourfuture.hyfshop.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +import java.net.URI; + +@Configuration +public class B2Config { + + @Value("${b2.endpoint}") + private String endpoint; + + @Value("${b2.region}") + private String region; + + @Value("${b2.access-key}") + private String accessKey; + + @Value("${b2.secret-key}") + private String secretKey; + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey))) + .endpointOverride(URI.create(endpoint)) + .region(Region.of(region)) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/FileService.java b/src/main/java/net/hackyourfuture/hyfshop/product/FileService.java new file mode 100644 index 0000000..e9227a3 --- /dev/null +++ b/src/main/java/net/hackyourfuture/hyfshop/product/FileService.java @@ -0,0 +1,52 @@ +package net.hackyourfuture.hyfshop.product; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.IOException; +import java.util.UUID; + +@Service +public class FileService { + + private final S3Client s3Client; + + @Value("${b2.bucket}") + private String bucket; + + @Value("${b2.public-url}") + private String publicUrl; + + public FileService(S3Client s3Client) { + this.s3Client = s3Client; + } + + public String upload(MultipartFile file) throws IOException { + String key = "uploads/" + UUID.randomUUID() + "-" + file.getOriginalFilename(); + + s3Client.putObject( + PutObjectRequest.builder() + .bucket(bucket) + .key(key) + .contentType(file.getContentType()) + .build(), + RequestBody.fromInputStream(file.getInputStream(), file.getSize()) + ); + + return publicUrl + "/" + key; + } + + public void delete(String key) { + s3Client.deleteObject( + DeleteObjectRequest.builder() + .bucket(bucket) + .key(key) + .build() + ); + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java index 4d1713a..c17154a 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java @@ -8,7 +8,7 @@ import tools.jackson.databind.ObjectMapper; import java.util.List; -import java.util.Map; + @Repository @AllArgsConstructor @@ -27,7 +27,7 @@ public class ProductRepository { String json = rs.getString("details"); if (json != null) { product.setDetails(objectMapper.readValue(json, - new TypeReference>() { + new TypeReference<>() { })); } } catch (Exception e) { @@ -81,8 +81,8 @@ OR jsonb_exists(details->'colors', ?) .list(); } - public Product setSize(int id, String size) { - return jdbcClient.sql(""" + public void setSize(int id, String size) { + jdbcClient.sql(""" UPDATE products SET details = jsonb_set(details, '{size}', to_jsonb(CAST(? AS text))) WHERE id = ? From 957a63a1fd0830bbb2cffe79d1b871bf67b88dcb Mon Sep 17 00:00:00 2001 From: Shadi Date: Mon, 27 Jul 2026 14:51:01 +0200 Subject: [PATCH 4/4] fix imports and remove localhost configuration --- pom.xml | 5 +++++ .../hackyourfuture/hyfshop/product/ProductRepository.java | 4 ++-- src/main/resources/application.yaml | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 78d0f07..53309fe 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,11 @@ org.springframework.boot spring-boot-starter-jdbc + + com.fasterxml.jackson.core + jackson-databind + 2.17.2 + tools.jackson.core jackson-core diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java index c17154a..cdf6971 100644 --- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java +++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java @@ -1,11 +1,11 @@ package net.hackyourfuture.hyfshop.product; +import com.fasterxml.jackson.core.type.TypeReference; import lombok.AllArgsConstructor; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.stereotype.Repository; -import tools.jackson.core.type.TypeReference; -import tools.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 703fea0..e822cd9 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -2,7 +2,7 @@ spring: application: name: hyf_shop datasource: - url: ${DB_URL:jdbc:postgresql://localhost:5432/hyfshop} + url: ${DB_URL} username: ${DB_USERNAME:hyfuser} password: ${DB_PASSWORD:hyfpassword}