Skip to content

Commit d5e973e

Browse files
committed
feat: separate feed and subscription data
1 parent 2118170 commit d5e973e

20 files changed

+422
-424
lines changed

compose.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
services:
22
postgres:
3-
image: 'postgres:latest'
3+
image: 'postgres:18'
44
env_file:
55
- .env
66
ports:
77
- '5432:5432'
88
volumes:
9-
- postgres_data:/var/lib/postgresql/data
9+
- postgres_data:/var/lib/postgresql
1010

1111
redis:
1212
image: 'redis:latest'
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.openpodcastapi.opa.feed;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import jakarta.validation.constraints.NotNull;
5+
import org.hibernate.validator.constraints.UUID;
6+
7+
/// DTO records for feed entities
8+
public class FeedDTO {
9+
/// A DTO representing a new subscription
10+
///
11+
/// @param feedUrl the URL of the feed
12+
/// @param uuid the UUID of the feed calculated by the client
13+
public record NewFeedRequestDTO(
14+
@JsonProperty(required = true) @NotNull @UUID String uuid,
15+
@JsonProperty(required = true) @NotNull String feedUrl
16+
) {
17+
}
18+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package org.openpodcastapi.opa.feed;
2+
3+
import jakarta.persistence.*;
4+
import org.openpodcastapi.opa.subscription.SubscriptionEntity;
5+
6+
import java.time.Instant;
7+
import java.util.Set;
8+
import java.util.UUID;
9+
10+
/// An entity representing podcast feed metadata
11+
@Entity
12+
@Table(name = "feeds")
13+
public class FeedEntity {
14+
/// The feed's database ID
15+
@Id
16+
@GeneratedValue(strategy = GenerationType.IDENTITY)
17+
private Long id;
18+
19+
/// The UUIDv5 identifier for the feed.
20+
@Column(unique = true, nullable = false, updatable = false, columnDefinition = "uuid")
21+
private UUID uuid;
22+
23+
/// The URL of the subscription feed
24+
@Column(nullable = false)
25+
private String feedUrl;
26+
27+
/// Linked subscriptions
28+
@OneToMany(mappedBy = "feed", cascade = CascadeType.REMOVE)
29+
private Set<SubscriptionEntity> subscriptions;
30+
31+
/// Creation timestamp
32+
private Instant createdAt;
33+
34+
/// Last update timestamp
35+
private Instant updatedAt;
36+
37+
/// No-args constructor
38+
public FeedEntity() {
39+
}
40+
41+
/// Required-args constructor
42+
/// @param uuid the calculated UUIDv5 identifier for the feed
43+
/// @param feedUrl the URL location of the feed's XML file
44+
public FeedEntity(UUID uuid, String feedUrl) {
45+
this.uuid = uuid;
46+
this.feedUrl = feedUrl;
47+
}
48+
49+
/// Updates timestamps before saving
50+
@PrePersist
51+
public void prePersist() {
52+
final Instant timestamp = Instant.now();
53+
this.setCreatedAt(timestamp);
54+
this.setUpdatedAt(timestamp);
55+
}
56+
57+
/// Updates the last updated timestamp when the entity is updated
58+
@PreUpdate
59+
public void preUpdate() {
60+
final Instant timestamp = Instant.now();
61+
this.setUpdatedAt(timestamp);
62+
}
63+
64+
/// @return the feed entity ID
65+
public Long getId() {
66+
return id;
67+
}
68+
69+
/// @return the feed's UUIDv5 identifier
70+
public UUID getUuid() {
71+
return uuid;
72+
}
73+
74+
/// @param uuid the calculated UUIDv5 identifier for the feed
75+
public void setUuid(UUID uuid) {
76+
this.uuid = uuid;
77+
}
78+
79+
/// @return the feed's URL
80+
public String getFeedUrl() {
81+
return feedUrl;
82+
}
83+
84+
/// @param feedUrl the feed's URL
85+
public void setFeedUrl(String feedUrl) {
86+
this.feedUrl = feedUrl;
87+
}
88+
89+
/// @return the `createdAt` timestamp for the feed entity
90+
public Instant getCreatedAt() {
91+
return createdAt;
92+
}
93+
94+
/// @param createdAt the creation timestamp
95+
public void setCreatedAt(Instant createdAt) {
96+
this.createdAt = createdAt;
97+
}
98+
99+
/// @return the `updatedAt` timestamp for the feed entity
100+
public Instant getUpdatedAt() {
101+
return updatedAt;
102+
}
103+
104+
/// @param updatedAt the new `updatedAt` timestamp
105+
public void setUpdatedAt(Instant updatedAt) {
106+
this.updatedAt = updatedAt;
107+
}
108+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.openpodcastapi.opa.feed;
2+
3+
import org.mapstruct.Mapper;
4+
import org.mapstruct.ReportingPolicy;
5+
6+
/// Helper class to map feed entities and DTOs
7+
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
8+
public interface FeedMapper {
9+
/// Maps an incoming feed creation request to an entity
10+
///
11+
/// @param dto the DTO to map
12+
/// @return a mapped [FeedEntity]
13+
FeedEntity toEntity(FeedDTO.NewFeedRequestDTO dto);
14+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.openpodcastapi.opa.feed;
2+
3+
import org.jspecify.annotations.NonNull;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.stereotype.Repository;
6+
7+
import java.util.Optional;
8+
import java.util.UUID;
9+
10+
/// Repository for subscription feed interactions
11+
@Repository
12+
public interface FeedRepository extends JpaRepository<@NonNull FeedEntity, @NonNull Long> {
13+
/// Finds a single subscription by UUID.
14+
///
15+
/// @param uuid the UUIDv5 value to match
16+
/// @return an optional [FeedEntity] match
17+
Optional<FeedEntity> findByUuid(UUID uuid);
18+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.openpodcastapi.opa.feed;
2+
3+
import org.slf4j.Logger;
4+
import org.springframework.stereotype.Service;
5+
6+
import java.util.UUID;
7+
8+
import static org.slf4j.LoggerFactory.getLogger;
9+
10+
/// Service for feed operations
11+
@Service
12+
public class FeedService {
13+
private static final Logger log = getLogger(FeedService.class);
14+
private final FeedRepository repository;
15+
private final FeedMapper mapper;
16+
17+
/// All-args constructor
18+
///
19+
/// @param repository the [FeedRepository] for database interactions
20+
/// @param mapper the [FeedMapper] for DTO mapping
21+
public FeedService(FeedRepository repository, FeedMapper mapper) {
22+
this.repository = repository;
23+
this.mapper = mapper;
24+
}
25+
26+
/// Fetches an existing repository from the database or creates a new one if none is found
27+
///
28+
/// @param dto the DTO containing the subscription data
29+
/// @return the fetched or created subscription
30+
public FeedEntity fetchOrCreateFeed(FeedDTO.NewFeedRequestDTO dto) {
31+
final var feedUuid = UUID.fromString(dto.uuid());
32+
33+
log.debug("Searching for existing feed with UUID {}", feedUuid);
34+
35+
return repository
36+
.findByUuid(feedUuid)
37+
.orElseGet(() -> {
38+
log.info("Creating new subscription with UUID {} and feed URL {}", dto.uuid(), dto.feedUrl());
39+
return repository.save(mapper.toEntity(dto));
40+
});
41+
}
42+
}

src/main/java/org/openpodcastapi/opa/subscription/SubscriptionDTO.java

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import com.fasterxml.jackson.annotation.JsonInclude;
44
import com.fasterxml.jackson.annotation.JsonProperty;
55
import jakarta.annotation.Nullable;
6-
import jakarta.validation.constraints.NotNull;
76
import org.hibernate.validator.constraints.URL;
87
import org.hibernate.validator.constraints.UUID;
98
import org.jspecify.annotations.NonNull;
@@ -14,16 +13,6 @@
1413

1514
/// Container for all subscription-related data transfer objects
1615
public class SubscriptionDTO {
17-
/// A DTO representing a new subscription
18-
///
19-
/// @param feedUrl the URL of the feed
20-
/// @param uuid the UUID of the feed calculated by the client
21-
public record SubscriptionCreateDTO(
22-
@JsonProperty(required = true) @NotNull @UUID String uuid,
23-
@JsonProperty(required = true) @NotNull String feedUrl
24-
) {
25-
}
26-
2716
/// A DTO representing a user's subscription to a given feed
2817
///
2918
/// @param uuid the feed UUID

0 commit comments

Comments
 (0)