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: 2 additions & 1 deletion databases/grocery-db/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ CREATE TABLE IF NOT EXISTS grocery_lists (
grocery_list_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
outdated BOOLEAN NOT NULL DEFAULT FALSE,
user_id UUID NOT NULL
user_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS grocery_list_recipes (
Expand Down
6 changes: 6 additions & 0 deletions databases/user-db/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ CREATE TABLE IF NOT EXISTS users (
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Dev admin account: login with admin@bytebite.dev / password
INSERT INTO users (name, email, password_hash) VALUES (
'Admin',
'admin@bytebite.dev',
'$2b$12$F0rLm1sYDRHEUjQkni8z3e7dpeFNW4irxo4jkMgP4YMGffswtD.OS'
) ON CONFLICT (email) DO NOTHING;
2 changes: 1 addition & 1 deletion server/api-gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ spring:
- id: grocery-service
uri: ${GROCERY_SERVICE_BASE_URL:http://localhost:8082}
predicates:
- Path=/api/recipes/**
- Path=/api/recipes/**,/api/grocery-list/**
- id: user-service
uri: ${USER_SERVICE_BASE_URL:http://localhost:8083}
predicates:
Expand Down
11 changes: 11 additions & 0 deletions server/grocery-service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@
<version>${springdoc-openapi.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.bytebite.server;

import com.bytebite.server.dto.GroceryListDetailDTO;
import com.bytebite.server.dto.GroceryListSummaryDTO;
import com.bytebite.server.service.GroceryListService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;
import java.util.UUID;

@RestController
@RequestMapping("/api/grocery-list")
@Tag(name = "Grocery Lists", description = "Stored grocery-list history and details")
public class GroceryListController {

private final GroceryListService service;

public GroceryListController(GroceryListService service) {
this.service = service;
}

@GetMapping(value = "/history", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(
summary = "List recent grocery lists",
description = "Returns a summary of the 20 most recently created grocery lists, newest first.",
security = @SecurityRequirement(name = "bearerAuth"),
responses = {
@ApiResponse(responseCode = "200", description = "Grocery-list summaries"),
@ApiResponse(responseCode = "401", description = "Missing, expired, or invalid JWT", content = @Content)
}
)
public List<GroceryListSummaryDTO> getHistory() {
return service.getHistory();
}

@GetMapping(value = "/history/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(
summary = "Get a grocery list by id",
description = "Returns a single grocery list including its items.",
security = @SecurityRequirement(name = "bearerAuth"),
responses = {
@ApiResponse(responseCode = "200", description = "Grocery list with items"),
@ApiResponse(responseCode = "401", description = "Missing, expired, or invalid JWT", content = @Content),
@ApiResponse(responseCode = "404", description = "Grocery list not found", content = @Content(schema = @Schema(implementation = Map.class)))
}
)
public GroceryListDetailDTO getById(
@Parameter(description = "Identifier of the grocery list", required = true)
@PathVariable UUID id) {
return service.getById(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.bytebite.server.dto;

import java.util.UUID;

public record GroceryItemResponseDTO(UUID id, String name, double quantity, String unit, String category, boolean purchased) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.bytebite.server.dto;

import java.time.Instant;
import java.util.List;
import java.util.UUID;

public record GroceryListDetailDTO(UUID id, String name, Instant createdAt, List<GroceryItemResponseDTO> items) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.bytebite.server.dto;

import java.time.Instant;
import java.util.UUID;

public record GroceryListSummaryDTO(UUID id, String name, Instant createdAt) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.bytebite.server.entity;

public enum GroceryCategory {
PRODUCE,
DAIRY,
MEAT,
SEAFOOD,
BAKERY,
PANTRY,
FROZEN,
BEVERAGES,
SPICES,
OTHER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.bytebite.server.entity;

import jakarta.persistence.*;
import java.util.UUID;

@Entity
@Table(name = "grocery_items")
public class GroceryItem {

@Id
@Column(name = "item_id")
private UUID id;

@Column(nullable = false)
private String name;

@Column(nullable = false)
private double quantity;

@Column(nullable = false)
private String unit;

@Enumerated(EnumType.STRING)
@Column(columnDefinition = "grocery_category", nullable = false)
private GroceryCategory category;

@Column(name = "is_purchased", nullable = false)
private boolean purchased;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "grocery_list_id")
private GroceryList groceryList;

public UUID getId() { return id; }
public String getName() { return name; }
public double getQuantity() { return quantity; }
public String getUnit() { return unit; }
public GroceryCategory getCategory() { return category; }
public boolean isPurchased() { return purchased; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.bytebite.server.entity;

import jakarta.persistence.*;
import java.time.Instant;
import java.util.List;
import java.util.UUID;

@Entity
@Table(name = "grocery_lists")
public class GroceryList {

@Id
@Column(name = "grocery_list_id")
private UUID id;

@Column(nullable = false)
private String name;

@Column(nullable = false)
private boolean outdated;

@Column(name = "user_id", nullable = false)
private UUID userId;

@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;

@OneToMany(mappedBy = "groceryList", fetch = FetchType.LAZY)
private List<GroceryItem> items;

public UUID getId() { return id; }
public String getName() { return name; }
public boolean isOutdated() { return outdated; }
public UUID getUserId() { return userId; }
public Instant getCreatedAt() { return createdAt; }
public List<GroceryItem> getItems() { return items; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.bytebite.server.repository;

import com.bytebite.server.entity.GroceryList;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.UUID;

@Repository
public interface GroceryListRepository extends JpaRepository<GroceryList, UUID> {

List<GroceryList> findTop20ByOrderByCreatedAtDesc();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.bytebite.server.service;

import com.bytebite.server.dto.GroceryItemResponseDTO;
import com.bytebite.server.dto.GroceryListDetailDTO;
import com.bytebite.server.dto.GroceryListSummaryDTO;
import com.bytebite.server.entity.GroceryList;
import com.bytebite.server.repository.GroceryListRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;
import java.util.UUID;

@Service
public class GroceryListService {

private final GroceryListRepository repository;

public GroceryListService(GroceryListRepository repository) {
this.repository = repository;
}

@Transactional(readOnly = true)
public List<GroceryListSummaryDTO> getHistory() {
return repository.findTop20ByOrderByCreatedAtDesc().stream()
.map(gl -> new GroceryListSummaryDTO(gl.getId(), gl.getName(), gl.getCreatedAt()))
.toList();
}

@Transactional(readOnly = true)
public GroceryListDetailDTO getById(UUID id) {
GroceryList groceryList = repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Grocery list not found: " + id));

List<GroceryItemResponseDTO> items = groceryList.getItems().stream()
.map(item -> new GroceryItemResponseDTO(
item.getId(),
item.getName(),
item.getQuantity(),
item.getUnit(),
item.getCategory().name(),
item.isPurchased()
))
.toList();

return new GroceryListDetailDTO(groceryList.getId(), groceryList.getName(), groceryList.getCreatedAt(), items);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
spring.application.name=grocery-service
server.port=8082
genai.base-url=http://localhost:8000

spring.datasource.url=${GROCERY_DB_URL:jdbc:postgresql://localhost:5432/grocerydb}
spring.datasource.username=${GROCERY_DB_USER:postgres}
spring.datasource.password=${GROCERY_DB_PASSWORD:postgres}
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
Loading