diff --git a/pom.xml b/pom.xml
index bbdda38..53309fe 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
@@ -63,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/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/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..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;
@@ -21,20 +20,21 @@ 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) {
+ 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 2766734..cdf6971 100644
--- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java
+++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java
@@ -1,38 +1,58 @@
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 com.fasterxml.jackson.databind.ObjectMapper;
+
import java.util.List;
+
@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 +69,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");
+ public void setSize(int id, String size) {
+ 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/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/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..e822cd9 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -2,8 +2,17 @@ spring:
application:
name: hyf_shop
datasource:
- url: '${DB_URL}'
- username: '${DB_USERNAME}'
- password: '${DB_PASSWORD}'
+ url: ${DB_URL}
+ 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