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:
JSLMPR 2026-07-08 22:44:18 +02:00
parent 067fc782a3
commit 517a4e1146
21 changed files with 1111 additions and 0 deletions

18
pom.xml
View File

@ -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>

View File

@ -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<>();

View File

@ -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()
);
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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> {
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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

View File

@ -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

View File

@ -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
);