The service now has an opt-in JPA/Flyway adapter instead of only the in-memory repository. I introduced VideoClippingRepository as the application-facing persistence contract and wired both src/
main/java/org/example/videoclips/infrastructure/InMemoryVideoAssetRepository.java and the new src/main/java/org/example/videoclips/persistence/JpaVideoClippingRepository.java behind it. The JPA path includes entity models, Spring Data repositories, a mapper, and a Flyway baseline migration at src/main/resources/db/migration/V1__init_video_clipping.sql. I also added the required dependencies in pom.xml. The default runtime still uses the in-memory repository via video-clipping.repository=memory, so existing behavior stays intact. There’s now a ready-to-use JPA profile in src/main/resources/ application-jpa.properties that uses H2 in PostgreSQL mode for local bring-up. That gives the codebase a real persistence lane without forcing the switch yet.
This commit is contained in:
parent
067fc782a3
commit
517a4e1146
18
pom.xml
18
pom.xml
|
|
@ -26,6 +26,10 @@
|
|||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
|
|
@ -34,6 +38,20 @@
|
|||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import org.example.videoclips.domain.ClipJobStatus;
|
|||
import org.example.videoclips.domain.JobEvent;
|
||||
import org.example.videoclips.domain.UploadSession;
|
||||
import org.example.videoclips.domain.VideoAsset;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -15,6 +16,7 @@ import java.util.Map;
|
|||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Repository
|
||||
@ConditionalOnProperty(name = "video-clipping.repository", havingValue = "memory", matchIfMissing = true)
|
||||
public class InMemoryVideoAssetRepository implements VideoClippingRepository {
|
||||
|
||||
private final Map<String, VideoAsset> assets = new ConcurrentHashMap<>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,155 @@
|
|||
package org.example.videoclips.persistence;
|
||||
|
||||
import org.example.videoclips.domain.AccuracyMode;
|
||||
import org.example.videoclips.domain.Clip;
|
||||
import org.example.videoclips.domain.ClipJob;
|
||||
import org.example.videoclips.domain.ClipJobStatus;
|
||||
import org.example.videoclips.domain.JobEvent;
|
||||
import org.example.videoclips.domain.UploadSession;
|
||||
import org.example.videoclips.domain.VideoAsset;
|
||||
import org.example.videoclips.persistence.entity.ClipEntity;
|
||||
import org.example.videoclips.persistence.entity.ClipJobEntity;
|
||||
import org.example.videoclips.persistence.entity.JobEventEntity;
|
||||
import org.example.videoclips.persistence.entity.UploadSessionEntity;
|
||||
import org.example.videoclips.persistence.entity.VideoAssetEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class JpaVideoClippingMapper {
|
||||
|
||||
public VideoAssetEntity toEntity(VideoAsset asset) {
|
||||
VideoAssetEntity entity = new VideoAssetEntity();
|
||||
entity.setId(asset.id());
|
||||
entity.setFileName(asset.fileName());
|
||||
entity.setContentType(asset.contentType());
|
||||
entity.setContentLengthBytes(asset.contentLengthBytes());
|
||||
entity.setChecksumSha256(asset.checksumSha256());
|
||||
entity.setClipProfile(asset.clipProfile());
|
||||
entity.setStatus(asset.status().name());
|
||||
entity.setUploadId(asset.uploadId());
|
||||
entity.setSourceDurationSeconds(asset.sourceDurationSeconds());
|
||||
entity.setCreatedAt(asset.createdAt());
|
||||
entity.setUploadedAt(asset.uploadedAt());
|
||||
return entity;
|
||||
}
|
||||
|
||||
public VideoAsset toDomain(VideoAssetEntity entity) {
|
||||
return new VideoAsset(
|
||||
entity.getId(),
|
||||
entity.getFileName(),
|
||||
entity.getContentType(),
|
||||
entity.getContentLengthBytes(),
|
||||
entity.getChecksumSha256(),
|
||||
entity.getClipProfile(),
|
||||
VideoAsset.Status.valueOf(entity.getStatus()),
|
||||
entity.getUploadId(),
|
||||
entity.getSourceDurationSeconds(),
|
||||
entity.getCreatedAt(),
|
||||
entity.getUploadedAt()
|
||||
);
|
||||
}
|
||||
|
||||
public UploadSessionEntity toEntity(UploadSession session) {
|
||||
UploadSessionEntity entity = new UploadSessionEntity();
|
||||
entity.setId(session.id());
|
||||
entity.setAssetId(session.assetId());
|
||||
entity.setStatus(session.status().name());
|
||||
entity.setPartSizeBytes(session.partSizeBytes());
|
||||
entity.setPartCount(session.partCount());
|
||||
entity.setExpiresAt(session.expiresAt());
|
||||
entity.setCompletedAt(session.completedAt());
|
||||
return entity;
|
||||
}
|
||||
|
||||
public UploadSession toDomain(UploadSessionEntity entity) {
|
||||
return new UploadSession(
|
||||
entity.getId(),
|
||||
entity.getAssetId(),
|
||||
UploadSession.Status.valueOf(entity.getStatus()),
|
||||
entity.getPartSizeBytes(),
|
||||
entity.getPartCount(),
|
||||
entity.getExpiresAt(),
|
||||
entity.getCompletedAt()
|
||||
);
|
||||
}
|
||||
|
||||
public ClipJobEntity toEntity(ClipJob job) {
|
||||
ClipJobEntity entity = new ClipJobEntity();
|
||||
entity.setId(job.id());
|
||||
entity.setAssetId(job.assetId());
|
||||
entity.setStatus(job.status().name());
|
||||
entity.setAccuracyMode(job.accuracyMode().name());
|
||||
entity.setSegmentDurationSeconds(job.segmentDurationSeconds());
|
||||
entity.setProgressPercent(job.progressPercent());
|
||||
entity.setAttemptCount(job.attemptCount());
|
||||
entity.setCreatedAt(job.createdAt());
|
||||
entity.setStartedAt(job.startedAt());
|
||||
entity.setFinishedAt(job.finishedAt());
|
||||
entity.setErrorMessage(job.errorMessage());
|
||||
return entity;
|
||||
}
|
||||
|
||||
public ClipJob toDomain(ClipJobEntity entity) {
|
||||
return new ClipJob(
|
||||
entity.getId(),
|
||||
entity.getAssetId(),
|
||||
ClipJobStatus.valueOf(entity.getStatus()),
|
||||
AccuracyMode.valueOf(entity.getAccuracyMode()),
|
||||
entity.getSegmentDurationSeconds(),
|
||||
entity.getProgressPercent(),
|
||||
entity.getAttemptCount(),
|
||||
entity.getCreatedAt(),
|
||||
entity.getStartedAt(),
|
||||
entity.getFinishedAt(),
|
||||
entity.getErrorMessage()
|
||||
);
|
||||
}
|
||||
|
||||
public ClipEntity toEntity(Clip clip) {
|
||||
ClipEntity entity = new ClipEntity();
|
||||
entity.setId(clip.id());
|
||||
entity.setJobId(clip.jobId());
|
||||
entity.setAssetId(clip.assetId());
|
||||
entity.setClipIndex(clip.clipIndex());
|
||||
entity.setStartSeconds(clip.startSeconds());
|
||||
entity.setDurationSeconds(clip.durationSeconds());
|
||||
entity.setContentType(clip.contentType());
|
||||
entity.setContentLengthBytes(clip.contentLengthBytes());
|
||||
entity.setCreatedAt(clip.createdAt());
|
||||
return entity;
|
||||
}
|
||||
|
||||
public Clip toDomain(ClipEntity entity) {
|
||||
return new Clip(
|
||||
entity.getId(),
|
||||
entity.getJobId(),
|
||||
entity.getAssetId(),
|
||||
entity.getClipIndex(),
|
||||
entity.getStartSeconds(),
|
||||
entity.getDurationSeconds(),
|
||||
entity.getContentType(),
|
||||
entity.getContentLengthBytes(),
|
||||
entity.getCreatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
public JobEventEntity toEntity(JobEvent event) {
|
||||
JobEventEntity entity = new JobEventEntity();
|
||||
entity.setId(event.id());
|
||||
entity.setAggregateId(event.jobId());
|
||||
entity.setEventType(event.eventType());
|
||||
entity.setMessage(event.message());
|
||||
entity.setCreatedAt(event.createdAt());
|
||||
return entity;
|
||||
}
|
||||
|
||||
public JobEvent toDomain(JobEventEntity entity) {
|
||||
return new JobEvent(
|
||||
entity.getId(),
|
||||
entity.getAggregateId(),
|
||||
entity.getEventType(),
|
||||
entity.getMessage(),
|
||||
entity.getCreatedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
package org.example.videoclips.persistence;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.example.videoclips.application.VideoClippingRepository;
|
||||
import org.example.videoclips.domain.Clip;
|
||||
import org.example.videoclips.domain.ClipJob;
|
||||
import org.example.videoclips.domain.ClipJobStatus;
|
||||
import org.example.videoclips.domain.JobEvent;
|
||||
import org.example.videoclips.domain.UploadSession;
|
||||
import org.example.videoclips.domain.VideoAsset;
|
||||
import org.example.videoclips.persistence.entity.AssetIdempotencyEntity;
|
||||
import org.example.videoclips.persistence.entity.JobIdempotencyEntity;
|
||||
import org.example.videoclips.persistence.spring.AssetIdempotencyJpaRepository;
|
||||
import org.example.videoclips.persistence.spring.ClipJobJpaRepository;
|
||||
import org.example.videoclips.persistence.spring.ClipJpaRepository;
|
||||
import org.example.videoclips.persistence.spring.JobEventJpaRepository;
|
||||
import org.example.videoclips.persistence.spring.JobIdempotencyJpaRepository;
|
||||
import org.example.videoclips.persistence.spring.UploadSessionJpaRepository;
|
||||
import org.example.videoclips.persistence.spring.VideoAssetJpaRepository;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Repository
|
||||
@ConditionalOnProperty(name = "video-clipping.repository", havingValue = "jpa")
|
||||
public class JpaVideoClippingRepository implements VideoClippingRepository {
|
||||
|
||||
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
private final VideoAssetJpaRepository videoAssetJpaRepository;
|
||||
private final UploadSessionJpaRepository uploadSessionJpaRepository;
|
||||
private final ClipJobJpaRepository clipJobJpaRepository;
|
||||
private final ClipJpaRepository clipJpaRepository;
|
||||
private final JobEventJpaRepository jobEventJpaRepository;
|
||||
private final AssetIdempotencyJpaRepository assetIdempotencyJpaRepository;
|
||||
private final JobIdempotencyJpaRepository jobIdempotencyJpaRepository;
|
||||
private final JpaVideoClippingMapper mapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public JpaVideoClippingRepository(
|
||||
VideoAssetJpaRepository videoAssetJpaRepository,
|
||||
UploadSessionJpaRepository uploadSessionJpaRepository,
|
||||
ClipJobJpaRepository clipJobJpaRepository,
|
||||
ClipJpaRepository clipJpaRepository,
|
||||
JobEventJpaRepository jobEventJpaRepository,
|
||||
AssetIdempotencyJpaRepository assetIdempotencyJpaRepository,
|
||||
JobIdempotencyJpaRepository jobIdempotencyJpaRepository,
|
||||
JpaVideoClippingMapper mapper,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.videoAssetJpaRepository = videoAssetJpaRepository;
|
||||
this.uploadSessionJpaRepository = uploadSessionJpaRepository;
|
||||
this.clipJobJpaRepository = clipJobJpaRepository;
|
||||
this.clipJpaRepository = clipJpaRepository;
|
||||
this.jobEventJpaRepository = jobEventJpaRepository;
|
||||
this.assetIdempotencyJpaRepository = assetIdempotencyJpaRepository;
|
||||
this.jobIdempotencyJpaRepository = jobIdempotencyJpaRepository;
|
||||
this.mapper = mapper;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAsset(VideoAsset asset) {
|
||||
videoAssetJpaRepository.save(mapper.toEntity(asset));
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoAsset findAssetById(String assetId) {
|
||||
return videoAssetJpaRepository.findById(assetId).map(mapper::toDomain).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveUploadSession(UploadSession uploadSession) {
|
||||
uploadSessionJpaRepository.save(mapper.toEntity(uploadSession));
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadSession findUploadSessionById(String uploadSessionId) {
|
||||
return uploadSessionJpaRepository.findById(uploadSessionId).map(mapper::toDomain).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveJob(ClipJob job) {
|
||||
clipJobJpaRepository.save(mapper.toEntity(job));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClipJob findJobById(String jobId) {
|
||||
return clipJobJpaRepository.findById(jobId).map(mapper::toDomain).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ClipJob> findJobsByAssetId(String assetId) {
|
||||
return clipJobJpaRepository.findByAssetId(assetId).stream().map(mapper::toDomain).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveClip(Clip clip) {
|
||||
clipJpaRepository.save(mapper.toEntity(clip));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Clip> findClipsByJobId(String jobId) {
|
||||
return clipJpaRepository.findByJobId(jobId).stream().map(mapper::toDomain).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Clip findClipById(String clipId) {
|
||||
return clipJpaRepository.findById(clipId).map(mapper::toDomain).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveJobEvent(JobEvent event) {
|
||||
jobEventJpaRepository.save(mapper.toEntity(event));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JobEvent> findEventsByAggregateId(String aggregateId) {
|
||||
return jobEventJpaRepository.findByAggregateId(aggregateId).stream().map(mapper::toDomain).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasActiveJobForAsset(String assetId) {
|
||||
return clipJobJpaRepository.findByAssetId(assetId).stream()
|
||||
.map(mapper::toDomain)
|
||||
.anyMatch(job -> job.status() != ClipJobStatus.SUCCEEDED
|
||||
&& job.status() != ClipJobStatus.FAILED
|
||||
&& job.status() != ClipJobStatus.CANCELLED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> findAssetResponseByIdempotencyKey(String key) {
|
||||
return assetIdempotencyJpaRepository.findById(key)
|
||||
.map(AssetIdempotencyEntity::getResponseJson)
|
||||
.map(this::deserialize)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeAssetResponseByIdempotencyKey(String key, Map<String, Object> response) {
|
||||
AssetIdempotencyEntity entity = new AssetIdempotencyEntity();
|
||||
entity.setIdempotencyKey(key);
|
||||
entity.setResponseJson(serialize(response));
|
||||
assetIdempotencyJpaRepository.save(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> findJobResponseByIdempotencyKey(String key) {
|
||||
return jobIdempotencyJpaRepository.findById(key)
|
||||
.map(JobIdempotencyEntity::getResponseJson)
|
||||
.map(this::deserialize)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeJobResponseByIdempotencyKey(String key, Map<String, Object> response) {
|
||||
JobIdempotencyEntity entity = new JobIdempotencyEntity();
|
||||
entity.setIdempotencyKey(key);
|
||||
entity.setResponseJson(serialize(response));
|
||||
jobIdempotencyJpaRepository.save(entity);
|
||||
}
|
||||
|
||||
private String serialize(Map<String, Object> response) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(response);
|
||||
} catch (JsonProcessingException ex) {
|
||||
throw new IllegalStateException("Failed to serialize idempotency response", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> deserialize(String responseJson) {
|
||||
try {
|
||||
return objectMapper.readValue(responseJson, MAP_TYPE);
|
||||
} catch (JsonProcessingException ex) {
|
||||
throw new IllegalStateException("Failed to deserialize idempotency response", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package org.example.videoclips.persistence.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "asset_idempotency")
|
||||
public class AssetIdempotencyEntity {
|
||||
|
||||
@Id
|
||||
@Column(length = 255)
|
||||
private String idempotencyKey;
|
||||
|
||||
@Lob
|
||||
@Column(nullable = false)
|
||||
private String responseJson;
|
||||
|
||||
public String getIdempotencyKey() {
|
||||
return idempotencyKey;
|
||||
}
|
||||
|
||||
public void setIdempotencyKey(String idempotencyKey) {
|
||||
this.idempotencyKey = idempotencyKey;
|
||||
}
|
||||
|
||||
public String getResponseJson() {
|
||||
return responseJson;
|
||||
}
|
||||
|
||||
public void setResponseJson(String responseJson) {
|
||||
this.responseJson = responseJson;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package org.example.videoclips.persistence.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "clips")
|
||||
public class ClipEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String jobId;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String assetId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int clipIndex;
|
||||
|
||||
@Column(nullable = false)
|
||||
private long startSeconds;
|
||||
|
||||
@Column(nullable = false)
|
||||
private long durationSeconds;
|
||||
|
||||
@Column(nullable = false, length = 100)
|
||||
private String contentType;
|
||||
|
||||
@Column(nullable = false)
|
||||
private long contentLengthBytes;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getJobId() {
|
||||
return jobId;
|
||||
}
|
||||
|
||||
public void setJobId(String jobId) {
|
||||
this.jobId = jobId;
|
||||
}
|
||||
|
||||
public String getAssetId() {
|
||||
return assetId;
|
||||
}
|
||||
|
||||
public void setAssetId(String assetId) {
|
||||
this.assetId = assetId;
|
||||
}
|
||||
|
||||
public int getClipIndex() {
|
||||
return clipIndex;
|
||||
}
|
||||
|
||||
public void setClipIndex(int clipIndex) {
|
||||
this.clipIndex = clipIndex;
|
||||
}
|
||||
|
||||
public long getStartSeconds() {
|
||||
return startSeconds;
|
||||
}
|
||||
|
||||
public void setStartSeconds(long startSeconds) {
|
||||
this.startSeconds = startSeconds;
|
||||
}
|
||||
|
||||
public long getDurationSeconds() {
|
||||
return durationSeconds;
|
||||
}
|
||||
|
||||
public void setDurationSeconds(long durationSeconds) {
|
||||
this.durationSeconds = durationSeconds;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public void setContentType(String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public long getContentLengthBytes() {
|
||||
return contentLengthBytes;
|
||||
}
|
||||
|
||||
public void setContentLengthBytes(long contentLengthBytes) {
|
||||
this.contentLengthBytes = contentLengthBytes;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
package org.example.videoclips.persistence.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "clip_jobs")
|
||||
public class ClipJobEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String assetId;
|
||||
|
||||
@Column(nullable = false, length = 32)
|
||||
private String status;
|
||||
|
||||
@Column(nullable = false, length = 32)
|
||||
private String accuracyMode;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int segmentDurationSeconds;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int progressPercent;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int attemptCount;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
private Instant startedAt;
|
||||
|
||||
private Instant finishedAt;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String errorMessage;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getAssetId() {
|
||||
return assetId;
|
||||
}
|
||||
|
||||
public void setAssetId(String assetId) {
|
||||
this.assetId = assetId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getAccuracyMode() {
|
||||
return accuracyMode;
|
||||
}
|
||||
|
||||
public void setAccuracyMode(String accuracyMode) {
|
||||
this.accuracyMode = accuracyMode;
|
||||
}
|
||||
|
||||
public int getSegmentDurationSeconds() {
|
||||
return segmentDurationSeconds;
|
||||
}
|
||||
|
||||
public void setSegmentDurationSeconds(int segmentDurationSeconds) {
|
||||
this.segmentDurationSeconds = segmentDurationSeconds;
|
||||
}
|
||||
|
||||
public int getProgressPercent() {
|
||||
return progressPercent;
|
||||
}
|
||||
|
||||
public void setProgressPercent(int progressPercent) {
|
||||
this.progressPercent = progressPercent;
|
||||
}
|
||||
|
||||
public int getAttemptCount() {
|
||||
return attemptCount;
|
||||
}
|
||||
|
||||
public void setAttemptCount(int attemptCount) {
|
||||
this.attemptCount = attemptCount;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Instant getStartedAt() {
|
||||
return startedAt;
|
||||
}
|
||||
|
||||
public void setStartedAt(Instant startedAt) {
|
||||
this.startedAt = startedAt;
|
||||
}
|
||||
|
||||
public Instant getFinishedAt() {
|
||||
return finishedAt;
|
||||
}
|
||||
|
||||
public void setFinishedAt(Instant finishedAt) {
|
||||
this.finishedAt = finishedAt;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package org.example.videoclips.persistence.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "job_events")
|
||||
public class JobEventEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String aggregateId;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String eventType;
|
||||
|
||||
@Column(nullable = false, length = 1000)
|
||||
private String message;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getAggregateId() {
|
||||
return aggregateId;
|
||||
}
|
||||
|
||||
public void setAggregateId(String aggregateId) {
|
||||
this.aggregateId = aggregateId;
|
||||
}
|
||||
|
||||
public String getEventType() {
|
||||
return eventType;
|
||||
}
|
||||
|
||||
public void setEventType(String eventType) {
|
||||
this.eventType = eventType;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package org.example.videoclips.persistence.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "job_idempotency")
|
||||
public class JobIdempotencyEntity {
|
||||
|
||||
@Id
|
||||
@Column(length = 255)
|
||||
private String idempotencyKey;
|
||||
|
||||
@Lob
|
||||
@Column(nullable = false)
|
||||
private String responseJson;
|
||||
|
||||
public String getIdempotencyKey() {
|
||||
return idempotencyKey;
|
||||
}
|
||||
|
||||
public void setIdempotencyKey(String idempotencyKey) {
|
||||
this.idempotencyKey = idempotencyKey;
|
||||
}
|
||||
|
||||
public String getResponseJson() {
|
||||
return responseJson;
|
||||
}
|
||||
|
||||
public void setResponseJson(String responseJson) {
|
||||
this.responseJson = responseJson;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package org.example.videoclips.persistence.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "upload_sessions")
|
||||
public class UploadSessionEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String assetId;
|
||||
|
||||
@Column(nullable = false, length = 32)
|
||||
private String status;
|
||||
|
||||
@Column(nullable = false)
|
||||
private long partSizeBytes;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int partCount;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Instant expiresAt;
|
||||
|
||||
private Instant completedAt;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getAssetId() {
|
||||
return assetId;
|
||||
}
|
||||
|
||||
public void setAssetId(String assetId) {
|
||||
this.assetId = assetId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public long getPartSizeBytes() {
|
||||
return partSizeBytes;
|
||||
}
|
||||
|
||||
public void setPartSizeBytes(long partSizeBytes) {
|
||||
this.partSizeBytes = partSizeBytes;
|
||||
}
|
||||
|
||||
public int getPartCount() {
|
||||
return partCount;
|
||||
}
|
||||
|
||||
public void setPartCount(int partCount) {
|
||||
this.partCount = partCount;
|
||||
}
|
||||
|
||||
public Instant getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(Instant expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public Instant getCompletedAt() {
|
||||
return completedAt;
|
||||
}
|
||||
|
||||
public void setCompletedAt(Instant completedAt) {
|
||||
this.completedAt = completedAt;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
package org.example.videoclips.persistence.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "video_assets")
|
||||
public class VideoAssetEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 255)
|
||||
private String fileName;
|
||||
|
||||
@Column(nullable = false, length = 100)
|
||||
private String contentType;
|
||||
|
||||
@Column(nullable = false)
|
||||
private long contentLengthBytes;
|
||||
|
||||
@Column(length = 64)
|
||||
private String checksumSha256;
|
||||
|
||||
@Column(nullable = false, length = 100)
|
||||
private String clipProfile;
|
||||
|
||||
@Column(nullable = false, length = 32)
|
||||
private String status;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String uploadId;
|
||||
|
||||
private Long sourceDurationSeconds;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
private Instant uploadedAt;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public void setContentType(String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public long getContentLengthBytes() {
|
||||
return contentLengthBytes;
|
||||
}
|
||||
|
||||
public void setContentLengthBytes(long contentLengthBytes) {
|
||||
this.contentLengthBytes = contentLengthBytes;
|
||||
}
|
||||
|
||||
public String getChecksumSha256() {
|
||||
return checksumSha256;
|
||||
}
|
||||
|
||||
public void setChecksumSha256(String checksumSha256) {
|
||||
this.checksumSha256 = checksumSha256;
|
||||
}
|
||||
|
||||
public String getClipProfile() {
|
||||
return clipProfile;
|
||||
}
|
||||
|
||||
public void setClipProfile(String clipProfile) {
|
||||
this.clipProfile = clipProfile;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getUploadId() {
|
||||
return uploadId;
|
||||
}
|
||||
|
||||
public void setUploadId(String uploadId) {
|
||||
this.uploadId = uploadId;
|
||||
}
|
||||
|
||||
public Long getSourceDurationSeconds() {
|
||||
return sourceDurationSeconds;
|
||||
}
|
||||
|
||||
public void setSourceDurationSeconds(Long sourceDurationSeconds) {
|
||||
this.sourceDurationSeconds = sourceDurationSeconds;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Instant getUploadedAt() {
|
||||
return uploadedAt;
|
||||
}
|
||||
|
||||
public void setUploadedAt(Instant uploadedAt) {
|
||||
this.uploadedAt = uploadedAt;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.example.videoclips.persistence.spring;
|
||||
|
||||
import org.example.videoclips.persistence.entity.AssetIdempotencyEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface AssetIdempotencyJpaRepository extends JpaRepository<AssetIdempotencyEntity, String> {
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package org.example.videoclips.persistence.spring;
|
||||
|
||||
import org.example.videoclips.persistence.entity.ClipJobEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ClipJobJpaRepository extends JpaRepository<ClipJobEntity, String> {
|
||||
|
||||
List<ClipJobEntity> findByAssetId(String assetId);
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package org.example.videoclips.persistence.spring;
|
||||
|
||||
import org.example.videoclips.persistence.entity.ClipEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ClipJpaRepository extends JpaRepository<ClipEntity, String> {
|
||||
|
||||
List<ClipEntity> findByJobId(String jobId);
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package org.example.videoclips.persistence.spring;
|
||||
|
||||
import org.example.videoclips.persistence.entity.JobEventEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface JobEventJpaRepository extends JpaRepository<JobEventEntity, String> {
|
||||
|
||||
List<JobEventEntity> findByAggregateId(String aggregateId);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.example.videoclips.persistence.spring;
|
||||
|
||||
import org.example.videoclips.persistence.entity.JobIdempotencyEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface JobIdempotencyJpaRepository extends JpaRepository<JobIdempotencyEntity, String> {
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.example.videoclips.persistence.spring;
|
||||
|
||||
import org.example.videoclips.persistence.entity.UploadSessionEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface UploadSessionJpaRepository extends JpaRepository<UploadSessionEntity, String> {
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.example.videoclips.persistence.spring;
|
||||
|
||||
import org.example.videoclips.persistence.entity.VideoAssetEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface VideoAssetJpaRepository extends JpaRepository<VideoAssetEntity, String> {
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
video-clipping.repository=jpa
|
||||
|
||||
spring.datasource.url=jdbc:h2:mem:videoclips;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
spring.datasource.driver-class-name=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
|
||||
spring.jpa.hibernate.ddl-auto=validate
|
||||
spring.jpa.show-sql=false
|
||||
spring.flyway.enabled=true
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
spring.application.name=video-clipping-service
|
||||
spring.mvc.problemdetails.enabled=true
|
||||
management.endpoints.web.exposure.include=health,info,metrics
|
||||
spring.jpa.open-in-view=false
|
||||
video-clipping.repository=memory
|
||||
video-clipping.max-file-size-bytes=53687091200
|
||||
video-clipping.multipart-part-size-bytes=104857600
|
||||
video-clipping.upload-url-ttl-minutes=60
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
create table if not exists video_assets (
|
||||
id varchar(64) primary key,
|
||||
file_name varchar(255) not null,
|
||||
content_type varchar(100) not null,
|
||||
content_length_bytes bigint not null,
|
||||
checksum_sha256 varchar(64),
|
||||
clip_profile varchar(100) not null,
|
||||
status varchar(32) not null,
|
||||
upload_id varchar(64) not null,
|
||||
source_duration_seconds bigint,
|
||||
created_at timestamp with time zone not null,
|
||||
uploaded_at timestamp with time zone
|
||||
);
|
||||
|
||||
create table if not exists upload_sessions (
|
||||
id varchar(64) primary key,
|
||||
asset_id varchar(64) not null,
|
||||
status varchar(32) not null,
|
||||
part_size_bytes bigint not null,
|
||||
part_count integer not null,
|
||||
expires_at timestamp with time zone not null,
|
||||
completed_at timestamp with time zone
|
||||
);
|
||||
|
||||
create table if not exists clip_jobs (
|
||||
id varchar(64) primary key,
|
||||
asset_id varchar(64) not null,
|
||||
status varchar(32) not null,
|
||||
accuracy_mode varchar(32) not null,
|
||||
segment_duration_seconds integer not null,
|
||||
progress_percent integer not null,
|
||||
attempt_count integer not null,
|
||||
created_at timestamp with time zone not null,
|
||||
started_at timestamp with time zone,
|
||||
finished_at timestamp with time zone,
|
||||
error_message varchar(1000)
|
||||
);
|
||||
|
||||
create index if not exists idx_clip_jobs_asset_id on clip_jobs(asset_id);
|
||||
create index if not exists idx_clip_jobs_status on clip_jobs(status);
|
||||
|
||||
create table if not exists clips (
|
||||
id varchar(64) primary key,
|
||||
job_id varchar(64) not null,
|
||||
asset_id varchar(64) not null,
|
||||
clip_index integer not null,
|
||||
start_seconds bigint not null,
|
||||
duration_seconds bigint not null,
|
||||
content_type varchar(100) not null,
|
||||
content_length_bytes bigint not null,
|
||||
created_at timestamp with time zone not null
|
||||
);
|
||||
|
||||
create unique index if not exists idx_clips_job_id_clip_index on clips(job_id, clip_index);
|
||||
create index if not exists idx_clips_asset_id on clips(asset_id);
|
||||
|
||||
create table if not exists job_events (
|
||||
id varchar(64) primary key,
|
||||
aggregate_id varchar(64) not null,
|
||||
event_type varchar(64) not null,
|
||||
message varchar(1000) not null,
|
||||
created_at timestamp with time zone not null
|
||||
);
|
||||
|
||||
create index if not exists idx_job_events_aggregate_id on job_events(aggregate_id);
|
||||
|
||||
create table if not exists asset_idempotency (
|
||||
idempotency_key varchar(255) primary key,
|
||||
response_json text not null
|
||||
);
|
||||
|
||||
create table if not exists job_idempotency (
|
||||
idempotency_key varchar(255) primary key,
|
||||
response_json text not null
|
||||
);
|
||||
Loading…
Reference in New Issue