autocomit

This commit is contained in:
2026-02-08 17:12:10 +01:00
parent 1e6450eed6
commit 8fef802597
23 changed files with 1311 additions and 7 deletions
@@ -0,0 +1,110 @@
package com.ldpv2.controller;
import com.ldpv2.dto.request.RecordDeploymentRequest;
import com.ldpv2.dto.response.CurrentDeploymentStateResponse;
import com.ldpv2.dto.response.DeploymentResponse;
import com.ldpv2.service.DeploymentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/deployments")
@Tag(name = "Deployments", description = "Deployment tracking endpoints")
@SecurityRequirement(name = "bearerAuth")
public class DeploymentController {
@Autowired
private DeploymentService deploymentService;
@PostMapping
@Operation(summary = "Record deployment", description = "Record a new deployment")
public ResponseEntity<DeploymentResponse> recordDeployment(
@Valid @RequestBody RecordDeploymentRequest request) {
DeploymentResponse response = deploymentService.recordDeployment(request);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@GetMapping("/{id}")
@Operation(summary = "Get deployment", description = "Get deployment by ID")
public ResponseEntity<DeploymentResponse> getById(@PathVariable UUID id) {
DeploymentResponse response = deploymentService.findById(id);
return ResponseEntity.ok(response);
}
@GetMapping
@Operation(summary = "List deployments", description = "Get paginated list of deployments with optional filters")
public ResponseEntity<Page<DeploymentResponse>> getAll(
@RequestParam(required = false) UUID applicationId,
@RequestParam(required = false) UUID environmentId,
@RequestParam(required = false) UUID versionId,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime dateFrom,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime dateTo,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "deploymentDate") String sortBy,
@RequestParam(defaultValue = "desc") String sortDirection) {
Sort sort = sortDirection.equalsIgnoreCase("desc")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
Page<DeploymentResponse> response;
if (applicationId != null || environmentId != null || versionId != null || dateFrom != null || dateTo != null) {
response = deploymentService.search(applicationId, environmentId, versionId, dateFrom, dateTo, pageable);
} else {
response = deploymentService.findAll(pageable);
}
return ResponseEntity.ok(response);
}
@GetMapping("/current")
@Operation(summary = "Get current state", description = "Get current deployment state across environments")
public ResponseEntity<List<CurrentDeploymentStateResponse>> getCurrentState(
@RequestParam(required = false) UUID applicationId,
@RequestParam(required = false) UUID environmentId) {
List<CurrentDeploymentStateResponse> response = deploymentService.getCurrentState(applicationId, environmentId);
return ResponseEntity.ok(response);
}
@GetMapping("/by-application/{applicationId}")
@Operation(summary = "Get deployments by application", description = "Get deployment history for an application")
public ResponseEntity<Page<DeploymentResponse>> getByApplication(
@PathVariable UUID applicationId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("deploymentDate").descending());
Page<DeploymentResponse> response = deploymentService.findByApplication(applicationId, pageable);
return ResponseEntity.ok(response);
}
@GetMapping("/by-environment/{environmentId}")
@Operation(summary = "Get deployments by environment", description = "Get all deployments to an environment")
public ResponseEntity<Page<DeploymentResponse>> getByEnvironment(
@PathVariable UUID environmentId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("deploymentDate").descending());
Page<DeploymentResponse> response = deploymentService.findByEnvironment(environmentId, pageable);
return ResponseEntity.ok(response);
}
}
@@ -0,0 +1,94 @@
package com.ldpv2.controller;
import com.ldpv2.dto.request.CreateVersionRequest;
import com.ldpv2.dto.request.UpdateVersionRequest;
import com.ldpv2.dto.response.VersionResponse;
import com.ldpv2.service.VersionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
import java.util.UUID;
@RestController
@RequestMapping("/applications/{applicationId}/versions")
@Tag(name = "Versions", description = "Version management endpoints")
@SecurityRequirement(name = "bearerAuth")
public class VersionController {
@Autowired
private VersionService versionService;
@PostMapping
@Operation(summary = "Create version", description = "Create a new version for an application")
public ResponseEntity<VersionResponse> create(
@PathVariable UUID applicationId,
@Valid @RequestBody CreateVersionRequest request) {
VersionResponse response = versionService.create(applicationId, request);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@PutMapping("/{id}")
@Operation(summary = "Update version", description = "Update an existing version")
public ResponseEntity<VersionResponse> update(
@PathVariable UUID applicationId,
@PathVariable UUID id,
@Valid @RequestBody UpdateVersionRequest request) {
VersionResponse response = versionService.update(id, request);
return ResponseEntity.ok(response);
}
@GetMapping("/{id}")
@Operation(summary = "Get version", description = "Get version by ID")
public ResponseEntity<VersionResponse> getById(
@PathVariable UUID applicationId,
@PathVariable UUID id) {
VersionResponse response = versionService.findById(id);
return ResponseEntity.ok(response);
}
@GetMapping
@Operation(summary = "List versions", description = "Get all versions for an application")
public ResponseEntity<Page<VersionResponse>> getAll(
@PathVariable UUID applicationId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "releaseDate") String sortBy,
@RequestParam(defaultValue = "desc") String sortDirection) {
Sort sort = sortDirection.equalsIgnoreCase("desc")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
Page<VersionResponse> response = versionService.findByApplication(applicationId, pageable);
return ResponseEntity.ok(response);
}
@GetMapping("/latest")
@Operation(summary = "Get latest version", description = "Get the most recent version for an application")
public ResponseEntity<VersionResponse> getLatest(@PathVariable UUID applicationId) {
Optional<VersionResponse> response = versionService.findLatestByApplication(applicationId);
return response.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
@Operation(summary = "Delete version", description = "Delete a version")
public ResponseEntity<Void> delete(
@PathVariable UUID applicationId,
@PathVariable UUID id) {
versionService.delete(id);
return ResponseEntity.noContent().build();
}
}
@@ -0,0 +1,42 @@
package com.ldpv2.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* Deployment entity - immutable record of deployments
* Tracks which version of which application is deployed to which environment
*/
@Data
@Entity
@Table(name = "deployment")
@NoArgsConstructor @AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Deployment extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "application_id", nullable = false)
private Application application;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "version_id", nullable = false)
private Version version;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "environment_id", nullable = false)
private Environment environment;
@Column(name = "deployment_date", nullable = false)
private LocalDateTime deploymentDate;
@Column(name = "deployed_by", length = 255)
private String deployedBy;
@Column(columnDefinition = "TEXT")
private String notes;
}
@@ -0,0 +1,40 @@
package com.ldpv2.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
/**
* Version entity representing application releases
*/
@Data
@Entity
@Table(name = "version", uniqueConstraints = {
@UniqueConstraint(name = "uk_version_app_identifier",
columnNames = {"application_id", "version_identifier"})
})
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Version extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "application_id", nullable = false)
private Application application;
@Column(name = "version_identifier", nullable = false, length = 100)
private String versionIdentifier;
@Column(name = "external_reference", length = 500)
private String externalReference;
@Column(name = "release_date", nullable = false)
private LocalDate releaseDate;
@Column(name = "end_of_life_date")
private LocalDate endOfLifeDate;
}
@@ -0,0 +1,28 @@
package com.ldpv2.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateVersionRequest {
@NotBlank(message = "Version identifier is required")
@Size(max = 100, message = "Version identifier must not exceed 100 characters")
private String versionIdentifier;
@Size(max = 500, message = "External reference must not exceed 500 characters")
private String externalReference;
@NotNull(message = "Release date is required")
private LocalDate releaseDate;
private LocalDate endOfLifeDate;
}
@@ -0,0 +1,31 @@
package com.ldpv2.dto.request;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RecordDeploymentRequest {
@NotNull(message = "Application ID is required")
private UUID applicationId;
@NotNull(message = "Version ID is required")
private UUID versionId;
@NotNull(message = "Environment ID is required")
private UUID environmentId;
@NotNull(message = "Deployment date is required")
private LocalDateTime deploymentDate;
private String deployedBy;
private String notes;
}
@@ -0,0 +1,24 @@
package com.ldpv2.dto.request;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UpdateVersionRequest {
@Size(max = 100, message = "Version identifier must not exceed 100 characters")
private String versionIdentifier;
@Size(max = 500, message = "External reference must not exceed 500 characters")
private String externalReference;
private LocalDate releaseDate;
private LocalDate endOfLifeDate;
}
@@ -0,0 +1,18 @@
package com.ldpv2.dto.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CurrentDeploymentStateResponse {
private ApplicationSummaryResponse application;
private EnvironmentSummaryResponse environment;
private VersionSummaryResponse version;
private LocalDateTime deploymentDate;
private String deployedBy;
}
@@ -0,0 +1,22 @@
package com.ldpv2.dto.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeploymentResponse {
private UUID id;
private ApplicationSummaryResponse application;
private VersionSummaryResponse version;
private EnvironmentSummaryResponse environment;
private LocalDateTime deploymentDate;
private String deployedBy;
private String notes;
private LocalDateTime createdAt;
}
@@ -0,0 +1,16 @@
package com.ldpv2.dto.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EnvironmentSummaryResponse {
private UUID id;
private String name;
private boolean isProduction;
}
@@ -0,0 +1,24 @@
package com.ldpv2.dto.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VersionResponse {
private UUID id;
private UUID applicationId;
private String applicationName;
private String versionIdentifier;
private String externalReference;
private LocalDate releaseDate;
private LocalDate endOfLifeDate;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,17 @@
package com.ldpv2.dto.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VersionSummaryResponse {
private UUID id;
private String versionIdentifier;
private LocalDate releaseDate;
}
@@ -0,0 +1,64 @@
package com.ldpv2.repository;
import com.ldpv2.domain.entity.Deployment;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface DeploymentRepository extends JpaRepository<Deployment, UUID> {
Page<Deployment> findByApplicationId(UUID applicationId, Pageable pageable);
Page<Deployment> findByEnvironmentId(UUID environmentId, Pageable pageable);
Page<Deployment> findByApplicationIdAndEnvironmentId(UUID applicationId, UUID environmentId, Pageable pageable);
@Query("SELECT d FROM Deployment d WHERE " +
"(:applicationId IS NULL OR d.application.id = :applicationId) AND " +
"(:environmentId IS NULL OR d.environment.id = :environmentId) AND " +
"(:versionId IS NULL OR d.version.id = :versionId) AND " +
"(:dateFrom IS NULL OR d.deploymentDate >= :dateFrom) AND " +
"(:dateTo IS NULL OR d.deploymentDate <= :dateTo)")
Page<Deployment> search(
@Param("applicationId") UUID applicationId,
@Param("environmentId") UUID environmentId,
@Param("versionId") UUID versionId,
@Param("dateFrom") LocalDateTime dateFrom,
@Param("dateTo") LocalDateTime dateTo,
Pageable pageable
);
/**
* Get current deployment state - most recent deployment per application/environment
*/
@Query("SELECT d FROM Deployment d WHERE d.id IN (" +
" SELECT MAX(d2.id) FROM Deployment d2 " +
" WHERE (:applicationId IS NULL OR d2.application.id = :applicationId) AND " +
" (:environmentId IS NULL OR d2.environment.id = :environmentId) " +
" GROUP BY d2.application.id, d2.environment.id" +
")")
List<Deployment> findCurrentState(
@Param("applicationId") UUID applicationId,
@Param("environmentId") UUID environmentId
);
/**
* Get current deployment for specific application in specific environment
*/
@Query("SELECT d FROM Deployment d " +
"WHERE d.application.id = :applicationId AND d.environment.id = :environmentId " +
"ORDER BY d.deploymentDate DESC LIMIT 1")
Optional<Deployment> findCurrentForApplicationInEnvironment(
@Param("applicationId") UUID applicationId,
@Param("environmentId") UUID environmentId
);
}
@@ -0,0 +1,25 @@
package com.ldpv2.repository;
import com.ldpv2.domain.entity.Version;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface VersionRepository extends JpaRepository<Version, UUID> {
Page<Version> findByApplicationId(UUID applicationId, Pageable pageable);
Optional<Version> findByApplicationIdAndVersionIdentifier(UUID applicationId, String versionIdentifier);
boolean existsByApplicationIdAndVersionIdentifier(UUID applicationId, String versionIdentifier);
@Query("SELECT v FROM Version v WHERE v.application.id = :applicationId ORDER BY v.releaseDate DESC LIMIT 1")
Optional<Version> findLatestByApplicationId(@Param("applicationId") UUID applicationId);
}
@@ -0,0 +1,189 @@
package com.ldpv2.service;
import com.ldpv2.domain.entity.Application;
import com.ldpv2.domain.entity.Deployment;
import com.ldpv2.domain.entity.Environment;
import com.ldpv2.domain.entity.Version;
import com.ldpv2.dto.request.RecordDeploymentRequest;
import com.ldpv2.dto.response.*;
import com.ldpv2.exception.BadRequestException;
import com.ldpv2.exception.ResourceNotFoundException;
import com.ldpv2.repository.ApplicationRepository;
import com.ldpv2.repository.DeploymentRepository;
import com.ldpv2.repository.EnvironmentRepository;
import com.ldpv2.repository.VersionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class DeploymentService {
@Autowired
private DeploymentRepository deploymentRepository;
@Autowired
private ApplicationRepository applicationRepository;
@Autowired
private VersionRepository versionRepository;
@Autowired
private EnvironmentRepository environmentRepository;
@Transactional
public DeploymentResponse recordDeployment(RecordDeploymentRequest request) {
// Validate application exists
Application application = applicationRepository.findById(request.getApplicationId())
.orElseThrow(() -> new ResourceNotFoundException(
"Application not found with id: " + request.getApplicationId()));
// Validate version exists
Version version = versionRepository.findById(request.getVersionId())
.orElseThrow(() -> new ResourceNotFoundException(
"Version not found with id: " + request.getVersionId()));
// Validate environment exists
Environment environment = environmentRepository.findById(request.getEnvironmentId())
.orElseThrow(() -> new ResourceNotFoundException(
"Environment not found with id: " + request.getEnvironmentId()));
// Validate that version belongs to the application
if (!version.getApplication().getId().equals(request.getApplicationId())) {
throw new BadRequestException(
"Version does not belong to the specified application");
}
// Validate deployment date is not in future
if (request.getDeploymentDate().isAfter(LocalDateTime.now())) {
throw new BadRequestException("Deployment date cannot be in the future");
}
Deployment deployment = new Deployment();
deployment.setApplication(application);
deployment.setVersion(version);
deployment.setEnvironment(environment);
deployment.setDeploymentDate(request.getDeploymentDate());
deployment.setDeployedBy(request.getDeployedBy());
deployment.setNotes(request.getNotes());
deployment = deploymentRepository.save(deployment);
return mapToResponse(deployment);
}
public DeploymentResponse findById(UUID id) {
Deployment deployment = deploymentRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException(
"Deployment not found with id: " + id));
return mapToResponse(deployment);
}
public Page<DeploymentResponse> findAll(Pageable pageable) {
return deploymentRepository.findAll(pageable).map(this::mapToResponse);
}
public Page<DeploymentResponse> findByApplication(UUID applicationId, Pageable pageable) {
if (!applicationRepository.existsById(applicationId)) {
throw new ResourceNotFoundException(
"Application not found with id: " + applicationId);
}
return deploymentRepository.findByApplicationId(applicationId, pageable)
.map(this::mapToResponse);
}
public Page<DeploymentResponse> findByEnvironment(UUID environmentId, Pageable pageable) {
if (!environmentRepository.existsById(environmentId)) {
throw new ResourceNotFoundException(
"Environment not found with id: " + environmentId);
}
return deploymentRepository.findByEnvironmentId(environmentId, pageable)
.map(this::mapToResponse);
}
public Page<DeploymentResponse> search(
UUID applicationId,
UUID environmentId,
UUID versionId,
LocalDateTime dateFrom,
LocalDateTime dateTo,
Pageable pageable) {
return deploymentRepository.search(
applicationId, environmentId, versionId, dateFrom, dateTo, pageable)
.map(this::mapToResponse);
}
public List<CurrentDeploymentStateResponse> getCurrentState(UUID applicationId, UUID environmentId) {
List<Deployment> deployments = deploymentRepository.findCurrentState(applicationId, environmentId);
return deployments.stream()
.map(this::mapToCurrentStateResponse)
.collect(Collectors.toList());
}
private DeploymentResponse mapToResponse(Deployment deployment) {
ApplicationSummaryResponse appSummary = new ApplicationSummaryResponse(
deployment.getApplication().getId(),
deployment.getApplication().getName(),
deployment.getApplication().getStatus(),
deployment.getApplication().getBusinessUnit().getName()
);
VersionSummaryResponse versionSummary = new VersionSummaryResponse(
deployment.getVersion().getId(),
deployment.getVersion().getVersionIdentifier(),
deployment.getVersion().getReleaseDate()
);
EnvironmentSummaryResponse envSummary = new EnvironmentSummaryResponse(
deployment.getEnvironment().getId(),
deployment.getEnvironment().getName(),
deployment.getEnvironment().getIsProduction()
);
return new DeploymentResponse(
deployment.getId(),
appSummary,
versionSummary,
envSummary,
deployment.getDeploymentDate(),
deployment.getDeployedBy(),
deployment.getNotes(),
deployment.getCreatedAt()
);
}
private CurrentDeploymentStateResponse mapToCurrentStateResponse(Deployment deployment) {
ApplicationSummaryResponse appSummary = new ApplicationSummaryResponse(
deployment.getApplication().getId(),
deployment.getApplication().getName(),
deployment.getApplication().getStatus(),
deployment.getApplication().getBusinessUnit().getName()
);
VersionSummaryResponse versionSummary = new VersionSummaryResponse(
deployment.getVersion().getId(),
deployment.getVersion().getVersionIdentifier(),
deployment.getVersion().getReleaseDate()
);
EnvironmentSummaryResponse envSummary = new EnvironmentSummaryResponse(
deployment.getEnvironment().getId(),
deployment.getEnvironment().getName(),
deployment.getEnvironment().getIsProduction()
);
return new CurrentDeploymentStateResponse(
appSummary,
envSummary,
versionSummary,
deployment.getDeploymentDate(),
deployment.getDeployedBy()
);
}
}
@@ -0,0 +1,160 @@
package com.ldpv2.service;
import com.ldpv2.domain.entity.Application;
import com.ldpv2.domain.entity.Version;
import com.ldpv2.dto.request.CreateVersionRequest;
import com.ldpv2.dto.request.UpdateVersionRequest;
import com.ldpv2.dto.response.VersionResponse;
import com.ldpv2.exception.BadRequestException;
import com.ldpv2.exception.ResourceNotFoundException;
import com.ldpv2.repository.ApplicationRepository;
import com.ldpv2.repository.VersionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.Optional;
import java.util.UUID;
@Service
public class VersionService {
@Autowired
private VersionRepository versionRepository;
@Autowired
private ApplicationRepository applicationRepository;
@Transactional
public VersionResponse create(UUID applicationId, CreateVersionRequest request) {
Application application = applicationRepository.findById(applicationId)
.orElseThrow(() -> new ResourceNotFoundException(
"Application not found with id: " + applicationId));
// Check if version identifier already exists for this application
if (versionRepository.existsByApplicationIdAndVersionIdentifier(
applicationId, request.getVersionIdentifier())) {
throw new BadRequestException(
"Version '" + request.getVersionIdentifier() +
"' already exists for this application");
}
// Validate dates
if (request.getEndOfLifeDate() != null &&
request.getReleaseDate().isAfter(request.getEndOfLifeDate())) {
throw new BadRequestException(
"End of life date must be after release date");
}
// Validate release date is not in future
if (request.getReleaseDate().isAfter(LocalDate.now())) {
throw new BadRequestException("Release date cannot be in the future");
}
Version version = new Version();
version.setApplication(application);
version.setVersionIdentifier(request.getVersionIdentifier());
version.setExternalReference(request.getExternalReference());
version.setReleaseDate(request.getReleaseDate());
version.setEndOfLifeDate(request.getEndOfLifeDate());
version = versionRepository.save(version);
return mapToResponse(version);
}
@Transactional
public VersionResponse update(UUID id, UpdateVersionRequest request) {
Version version = versionRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException(
"Version not found with id: " + id));
if (request.getVersionIdentifier() != null) {
// Check if new version identifier already exists for this application
if (!request.getVersionIdentifier().equals(version.getVersionIdentifier()) &&
versionRepository.existsByApplicationIdAndVersionIdentifier(
version.getApplication().getId(), request.getVersionIdentifier())) {
throw new BadRequestException(
"Version '" + request.getVersionIdentifier() +
"' already exists for this application");
}
version.setVersionIdentifier(request.getVersionIdentifier());
}
if (request.getExternalReference() != null) {
version.setExternalReference(request.getExternalReference());
}
if (request.getReleaseDate() != null) {
if (request.getReleaseDate().isAfter(LocalDate.now())) {
throw new BadRequestException("Release date cannot be in the future");
}
version.setReleaseDate(request.getReleaseDate());
}
if (request.getEndOfLifeDate() != null) {
version.setEndOfLifeDate(request.getEndOfLifeDate());
}
// Validate dates
if (version.getEndOfLifeDate() != null &&
version.getReleaseDate().isAfter(version.getEndOfLifeDate())) {
throw new BadRequestException(
"End of life date must be after release date");
}
version = versionRepository.save(version);
return mapToResponse(version);
}
public VersionResponse findById(UUID id) {
Version version = versionRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException(
"Version not found with id: " + id));
return mapToResponse(version);
}
public Page<VersionResponse> findByApplication(UUID applicationId, Pageable pageable) {
if (!applicationRepository.existsById(applicationId)) {
throw new ResourceNotFoundException(
"Application not found with id: " + applicationId);
}
return versionRepository.findByApplicationId(applicationId, pageable)
.map(this::mapToResponse);
}
public Optional<VersionResponse> findLatestByApplication(UUID applicationId) {
if (!applicationRepository.existsById(applicationId)) {
throw new ResourceNotFoundException(
"Application not found with id: " + applicationId);
}
return versionRepository.findLatestByApplicationId(applicationId)
.map(this::mapToResponse);
}
@Transactional
public void delete(UUID id) {
Version version = versionRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException(
"Version not found with id: " + id));
// Note: In production, check if version is deployed anywhere before deletion
versionRepository.delete(version);
}
private VersionResponse mapToResponse(Version version) {
return new VersionResponse(
version.getId(),
version.getApplication().getId(),
version.getApplication().getName(),
version.getVersionIdentifier(),
version.getExternalReference(),
version.getReleaseDate(),
version.getEndOfLifeDate(),
version.getCreatedAt(),
version.getUpdatedAt()
);
}
}
@@ -5,18 +5,13 @@
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<!-- Story 0: Foundation -->
<include file="db/changelog/v1.0/001-create-user-table.xml"/>
<include file="db/changelog/v1.0/002-create-environment-table.xml"/>
<include file="db/changelog/data/initial-data.xml"/>
<!-- Story 1: Business Units -->
<include file="db/changelog/v1.0/003-create-business-unit-table.xml"/>
<!-- Story 2: Applications -->
<include file="db/changelog/v1.0/004-create-application-table.xml"/>
<!-- Story 3: Contacts -->
<include file="db/changelog/v1.0/005-create-contact-tables.xml"/>
<include file="db/changelog/v1.0/006-create-version-table.xml"/>
<include file="db/changelog/v1.0/007-create-deployment-table.xml"/>
</databaseChangeLog>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="006-create-version-table" author="ldpv2-team">
<createTable tableName="version">
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="application_id" type="UUID">
<constraints nullable="false"
foreignKeyName="fk_version_application"
references="application(id)"
deleteCascade="true"/>
</column>
<column name="version_identifier" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="external_reference" type="VARCHAR(500)"/>
<column name="release_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="end_of_life_date" type="DATE"/>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="updated_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
</createTable>
<addUniqueConstraint
tableName="version"
columnNames="application_id, version_identifier"
constraintName="uk_version_app_identifier"/>
<createIndex tableName="version" indexName="idx_version_application">
<column name="application_id"/>
</createIndex>
<createIndex tableName="version" indexName="idx_version_release_date">
<column name="release_date"/>
</createIndex>
<!-- Sample data -->
<insert tableName="version">
<column name="application_id" valueComputed="(SELECT id FROM application WHERE name = 'Customer Portal' LIMIT 1)"/>
<column name="version_identifier" value="1.0.0"/>
<column name="external_reference" value="https://github.com/org/customer-portal/releases/tag/v1.0.0"/>
<column name="release_date" value="2024-01-15"/>
</insert>
<insert tableName="version">
<column name="application_id" valueComputed="(SELECT id FROM application WHERE name = 'Customer Portal' LIMIT 1)"/>
<column name="version_identifier" value="1.1.0"/>
<column name="release_date" value="2024-06-20"/>
</insert>
<insert tableName="version">
<column name="application_id" valueComputed="(SELECT id FROM application WHERE name = 'Customer Portal' LIMIT 1)"/>
<column name="version_identifier" value="1.2.0"/>
<column name="release_date" value="2025-01-10"/>
</insert>
<insert tableName="version">
<column name="application_id" valueComputed="(SELECT id FROM application WHERE name = 'Internal CRM' LIMIT 1)"/>
<column name="version_identifier" value="2.0.0"/>
<column name="release_date" value="2024-03-01"/>
</insert>
<insert tableName="version">
<column name="application_id" valueComputed="(SELECT id FROM application WHERE name = 'Internal CRM' LIMIT 1)"/>
<column name="version_identifier" value="2.1.0"/>
<column name="release_date" value="2024-09-15"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="007-create-deployment-table" author="ldpv2-team">
<createTable tableName="deployment">
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="application_id" type="UUID">
<constraints nullable="false"
foreignKeyName="fk_deployment_application"
references="application(id)"/>
</column>
<column name="version_id" type="UUID">
<constraints nullable="false"
foreignKeyName="fk_deployment_version"
references="version(id)"/>
</column>
<column name="environment_id" type="UUID">
<constraints nullable="false"
foreignKeyName="fk_deployment_environment"
references="environment(id)"/>
</column>
<column name="deployment_date" type="TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="deployed_by" type="VARCHAR(255)"/>
<column name="notes" type="TEXT"/>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
</createTable>
<createIndex tableName="deployment" indexName="idx_deployment_application">
<column name="application_id"/>
</createIndex>
<createIndex tableName="deployment" indexName="idx_deployment_environment">
<column name="environment_id"/>
</createIndex>
<createIndex tableName="deployment" indexName="idx_deployment_date">
<column name="deployment_date" descending="true"/>
</createIndex>
<createIndex tableName="deployment" indexName="idx_deployment_app_env">
<column name="application_id"/>
<column name="environment_id"/>
</createIndex>
<!-- Sample deployments -->
<insert tableName="deployment">
<column name="application_id" valueComputed="(SELECT id FROM application WHERE name = 'Customer Portal' LIMIT 1)"/>
<column name="version_id" valueComputed="(SELECT id FROM version WHERE version_identifier = '1.2.0' LIMIT 1)"/>
<column name="environment_id" valueComputed="(SELECT id FROM environment WHERE name = 'PROD-EU' LIMIT 1)"/>
<column name="deployment_date" value="2025-01-15T10:30:00"/>
<column name="deployed_by" value="admin"/>
<column name="notes" value="Production deployment"/>
</insert>
<insert tableName="deployment">
<column name="application_id" valueComputed="(SELECT id FROM application WHERE name = 'Internal CRM' LIMIT 1)"/>
<column name="version_id" valueComputed="(SELECT id FROM version WHERE version_identifier = '2.1.0' LIMIT 1)"/>
<column name="environment_id" valueComputed="(SELECT id FROM environment WHERE name = 'PROD-EU' LIMIT 1)"/>
<column name="deployment_date" value="2024-10-01T11:00:00"/>
<column name="deployed_by" value="admin"/>
<column name="notes" value="Major update"/>
</insert>
</changeSet>
</databaseChangeLog>