autocomit

This commit is contained in:
2026-02-09 19:33:55 +01:00
parent 16b8ae0b8e
commit fd356019d7
22 changed files with 1595 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
# Story 8: External Dependencies - Deployment Notes
## Files Created
### Backend
- Database migration: `009-create-external-dependency-tables.xml`
- Entities: `DependencyType.java`, `ExternalDependency.java`
- Repositories: `DependencyTypeRepository.java`, `ExternalDependencyRepository.java`
- Services: `DependencyTypeService.java`, `ExternalDependencyService.java`
- Controllers: `DependencyTypeController.java`, `ExternalDependencyController.java`
- DTOs: Request and Response classes for both entities
- Updated: `db.changelog-master.xml`
### Frontend
- Models: `dependency.model.ts`
- Service: `dependency.service.ts`
- Components:
- `application-dependencies` (tab in application detail)
- `dependency-list` (full list page)
- `dependency-form` (create/edit)
- `dependency-detail` (view details)
- `dependency-type-list` (admin catalog management)
## Deployment Steps
1. Copy all backend files to their respective locations
2. Run Liquibase migration: `mvn liquibase:update`
3. Build backend: `mvn clean package`
4. Copy frontend files
5. Install dependencies: `npm install` (if needed)
6. Build frontend: `ng build`
## Testing Checklist
- [ ] Default dependency types seeded
- [ ] Create custom dependency type (admin)
- [ ] Create external dependency
- [ ] Validate date logic
- [ ] Filter by type and status
- [ ] View expiring dependencies
- [ ] Update dependency
- [ ] Delete dependency
- [ ] Cannot delete type with dependencies
## API Endpoints
See Swagger UI at: http://localhost:8080/api/swagger-ui.html
Key endpoints:
- GET /api/dependency-types
- POST /api/dependency-types (admin)
- GET /api/dependencies
- POST /api/dependencies/for-application/{id}
- GET /api/dependencies/expiring?days=30
- GET /api/dependencies/expired
@@ -0,0 +1,68 @@
package com.ldpv2.controller;
import com.ldpv2.dto.request.CreateDependencyTypeRequest;
import com.ldpv2.dto.request.UpdateDependencyTypeRequest;
import com.ldpv2.dto.response.DependencyTypeResponse;
import com.ldpv2.service.DependencyTypeService;
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.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/dependency-types")
@Tag(name = "Dependency Types", description = "Dependency type catalog management")
@SecurityRequirement(name = "bearerAuth")
public class DependencyTypeController {
@Autowired
private DependencyTypeService dependencyTypeService;
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Create dependency type", description = "Create custom dependency type (Admin only)")
public ResponseEntity<DependencyTypeResponse> create(@Valid @RequestBody CreateDependencyTypeRequest request) {
DependencyTypeResponse response = dependencyTypeService.create(request);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Update dependency type", description = "Update dependency type (Admin only)")
public ResponseEntity<DependencyTypeResponse> update(
@PathVariable UUID id,
@Valid @RequestBody UpdateDependencyTypeRequest request) {
DependencyTypeResponse response = dependencyTypeService.update(id, request);
return ResponseEntity.ok(response);
}
@GetMapping("/{id}")
@Operation(summary = "Get dependency type", description = "Get dependency type by ID")
public ResponseEntity<DependencyTypeResponse> getById(@PathVariable UUID id) {
DependencyTypeResponse response = dependencyTypeService.findById(id);
return ResponseEntity.ok(response);
}
@GetMapping
@Operation(summary = "List dependency types", description = "Get all dependency types")
public ResponseEntity<List<DependencyTypeResponse>> getAll() {
List<DependencyTypeResponse> response = dependencyTypeService.findAll();
return ResponseEntity.ok(response);
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Delete dependency type", description = "Delete dependency type (Admin only)")
public ResponseEntity<Void> delete(@PathVariable UUID id) {
dependencyTypeService.delete(id);
return ResponseEntity.noContent().build();
}
}
@@ -0,0 +1,117 @@
package com.ldpv2.controller;
import com.ldpv2.dto.request.CreateExternalDependencyRequest;
import com.ldpv2.dto.request.UpdateExternalDependencyRequest;
import com.ldpv2.dto.response.ExternalDependencyResponse;
import com.ldpv2.service.ExternalDependencyService;
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.List;
import java.util.UUID;
@RestController
@RequestMapping("/dependencies")
@Tag(name = "External Dependencies", description = "External dependency management")
@SecurityRequirement(name = "bearerAuth")
public class ExternalDependencyController {
@Autowired
private ExternalDependencyService externalDependencyService;
@PostMapping("/for-application/{applicationId}")
@Operation(summary = "Create dependency", description = "Create external dependency for application")
public ResponseEntity<ExternalDependencyResponse> create(
@PathVariable UUID applicationId,
@Valid @RequestBody CreateExternalDependencyRequest request) {
ExternalDependencyResponse response = externalDependencyService.create(applicationId, request);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@PutMapping("/{id}")
@Operation(summary = "Update dependency", description = "Update external dependency")
public ResponseEntity<ExternalDependencyResponse> update(
@PathVariable UUID id,
@Valid @RequestBody UpdateExternalDependencyRequest request) {
ExternalDependencyResponse response = externalDependencyService.update(id, request);
return ResponseEntity.ok(response);
}
@GetMapping("/{id}")
@Operation(summary = "Get dependency", description = "Get external dependency by ID")
public ResponseEntity<ExternalDependencyResponse> getById(@PathVariable UUID id) {
ExternalDependencyResponse response = externalDependencyService.findById(id);
return ResponseEntity.ok(response);
}
@GetMapping
@Operation(summary = "List dependencies", description = "Get all dependencies with filters")
public ResponseEntity<Page<ExternalDependencyResponse>> getAll(
@RequestParam(required = false) UUID applicationId,
@RequestParam(required = false) UUID dependencyTypeId,
@RequestParam(required = false) String status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "name") String sortBy,
@RequestParam(defaultValue = "asc") String sortDirection) {
Sort sort = sortDirection.equalsIgnoreCase("desc")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
Page<ExternalDependencyResponse> response;
if (applicationId != null || dependencyTypeId != null || status != null) {
response = externalDependencyService.search(applicationId, dependencyTypeId, status, pageable);
} else {
response = externalDependencyService.findAll(pageable);
}
return ResponseEntity.ok(response);
}
@GetMapping("/by-application/{applicationId}")
@Operation(summary = "Get dependencies by application", description = "Get all dependencies for an application")
public ResponseEntity<Page<ExternalDependencyResponse>> getByApplication(
@PathVariable UUID applicationId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("name").ascending());
Page<ExternalDependencyResponse> response = externalDependencyService.findByApplication(applicationId, pageable);
return ResponseEntity.ok(response);
}
@GetMapping("/expiring")
@Operation(summary = "Get expiring dependencies", description = "Get dependencies expiring within specified days")
public ResponseEntity<List<ExternalDependencyResponse>> getExpiring(
@RequestParam(defaultValue = "30") int days) {
List<ExternalDependencyResponse> response = externalDependencyService.findExpiring(days);
return ResponseEntity.ok(response);
}
@GetMapping("/expired")
@Operation(summary = "Get expired dependencies", description = "Get all expired dependencies")
public ResponseEntity<List<ExternalDependencyResponse>> getExpired() {
List<ExternalDependencyResponse> response = externalDependencyService.findExpired();
return ResponseEntity.ok(response);
}
@DeleteMapping("/{id}")
@Operation(summary = "Delete dependency", description = "Delete external dependency")
public ResponseEntity<Void> delete(@PathVariable UUID id) {
externalDependencyService.delete(id);
return ResponseEntity.noContent().build();
}
}
@@ -0,0 +1,28 @@
package com.ldpv2.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* Dependency Type entity - catalog of dependency types
*/
@Data
@Entity
@Table(name = "dependency_type")
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class DependencyType extends BaseEntity {
@Column(name = "type_name", nullable = false, unique = true, length = 100)
private String typeName;
@Column(columnDefinition = "TEXT")
private String description;
@Column(name = "is_custom", nullable = false)
private Boolean isCustom = false;
}
@@ -0,0 +1,44 @@
package com.ldpv2.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
/**
* External Dependency entity
*/
@Data
@Entity
@Table(name = "external_dependency")
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ExternalDependency extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "application_id", nullable = false)
private Application application;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "dependency_type_id", nullable = false)
private DependencyType dependencyType;
@Column(nullable = false, length = 255)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(name = "technical_documentation", columnDefinition = "TEXT")
private String technicalDocumentation;
@Column(name = "validity_start_date")
private LocalDate validityStartDate;
@Column(name = "validity_end_date")
private LocalDate validityEndDate;
}
@@ -0,0 +1,19 @@
package com.ldpv2.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateDependencyTypeRequest {
@NotBlank(message = "Type name is required")
@Size(max = 100, message = "Type name must not exceed 100 characters")
private String typeName;
private String description;
}
@@ -0,0 +1,32 @@
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;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateExternalDependencyRequest {
@NotNull(message = "Dependency type is required")
private UUID dependencyTypeId;
@NotBlank(message = "Name is required")
@Size(max = 255, message = "Name must not exceed 255 characters")
private String name;
private String description;
private String technicalDocumentation;
private LocalDate validityStartDate;
private LocalDate validityEndDate;
}
@@ -0,0 +1,17 @@
package com.ldpv2.dto.request;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UpdateDependencyTypeRequest {
@Size(max = 100, message = "Type name must not exceed 100 characters")
private String typeName;
private String description;
}
@@ -0,0 +1,28 @@
package com.ldpv2.dto.request;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UpdateExternalDependencyRequest {
private UUID dependencyTypeId;
@Size(max = 255, message = "Name must not exceed 255 characters")
private String name;
private String description;
private String technicalDocumentation;
private LocalDate validityStartDate;
private LocalDate validityEndDate;
}
@@ -0,0 +1,20 @@
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 DependencyTypeResponse {
private UUID id;
private String typeName;
private String description;
private Boolean isCustom;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,28 @@
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 ExternalDependencyResponse {
private UUID id;
private ApplicationSummaryResponse application;
private DependencyTypeResponse dependencyType;
private String name;
private String description;
private String technicalDocumentation;
private LocalDate validityStartDate;
private LocalDate validityEndDate;
private Boolean isActive;
private Integer daysUntilExpiration;
private String status; // ACTIVE, EXPIRING, EXPIRED, NOT_YET_VALID
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,14 @@
package com.ldpv2.repository;
import com.ldpv2.domain.entity.DependencyType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface DependencyTypeRepository extends JpaRepository<DependencyType, UUID> {
Optional<DependencyType> findByTypeName(String typeName);
boolean existsByTypeName(String typeName);
}
@@ -0,0 +1,55 @@
package com.ldpv2.repository;
import com.ldpv2.domain.entity.ExternalDependency;
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.LocalDate;
import java.util.List;
import java.util.UUID;
@Repository
public interface ExternalDependencyRepository extends JpaRepository<ExternalDependency, UUID> {
Page<ExternalDependency> findByApplicationId(UUID applicationId, Pageable pageable);
Page<ExternalDependency> findByDependencyTypeId(UUID dependencyTypeId, Pageable pageable);
@Query("SELECT d FROM ExternalDependency d WHERE " +
"d.validityEndDate IS NOT NULL AND " +
"d.validityEndDate >= :now AND " +
"d.validityEndDate <= :expirationDate")
List<ExternalDependency> findExpiring(
@Param("now") LocalDate now,
@Param("expirationDate") LocalDate expirationDate
);
@Query("SELECT d FROM ExternalDependency d WHERE " +
"d.validityEndDate IS NOT NULL AND " +
"d.validityEndDate < :now")
List<ExternalDependency> findExpired(@Param("now") LocalDate now);
@Query("SELECT d FROM ExternalDependency d WHERE " +
"(:applicationId IS NULL OR d.application.id = :applicationId) AND " +
"(:dependencyTypeId IS NULL OR d.dependencyType.id = :dependencyTypeId) AND " +
"(:status IS NULL OR " +
" (:status = 'ACTIVE' AND (d.validityEndDate IS NULL OR d.validityEndDate >= :now) AND (d.validityStartDate IS NULL OR d.validityStartDate <= :now)) OR " +
" (:status = 'EXPIRING' AND d.validityEndDate IS NOT NULL AND d.validityEndDate >= :now AND d.validityEndDate <= :expiringDate) OR " +
" (:status = 'EXPIRED' AND d.validityEndDate IS NOT NULL AND d.validityEndDate < :now) OR " +
" (:status = 'NOT_YET_VALID' AND d.validityStartDate IS NOT NULL AND d.validityStartDate > :now)" +
")")
Page<ExternalDependency> search(
@Param("applicationId") UUID applicationId,
@Param("dependencyTypeId") UUID dependencyTypeId,
@Param("status") String status,
@Param("now") LocalDate now,
@Param("expiringDate") LocalDate expiringDate,
Pageable pageable
);
long countByDependencyTypeId(UUID dependencyTypeId);
}
@@ -0,0 +1,99 @@
package com.ldpv2.service;
import com.ldpv2.domain.entity.DependencyType;
import com.ldpv2.dto.request.CreateDependencyTypeRequest;
import com.ldpv2.dto.request.UpdateDependencyTypeRequest;
import com.ldpv2.dto.response.DependencyTypeResponse;
import com.ldpv2.exception.BadRequestException;
import com.ldpv2.exception.ResourceNotFoundException;
import com.ldpv2.repository.DependencyTypeRepository;
import com.ldpv2.repository.ExternalDependencyRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class DependencyTypeService {
@Autowired
private DependencyTypeRepository dependencyTypeRepository;
@Autowired
private ExternalDependencyRepository externalDependencyRepository;
@Transactional
public DependencyTypeResponse create(CreateDependencyTypeRequest request) {
if (dependencyTypeRepository.existsByTypeName(request.getTypeName())) {
throw new BadRequestException("Dependency type '" + request.getTypeName() + "' already exists");
}
DependencyType type = new DependencyType();
type.setTypeName(request.getTypeName());
type.setDescription(request.getDescription());
type.setIsCustom(true); // User-created types are custom
type = dependencyTypeRepository.save(type);
return mapToResponse(type);
}
@Transactional
public DependencyTypeResponse update(UUID id, UpdateDependencyTypeRequest request) {
DependencyType type = dependencyTypeRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Dependency type not found with id: " + id));
if (request.getTypeName() != null && !request.getTypeName().equals(type.getTypeName())) {
if (dependencyTypeRepository.existsByTypeName(request.getTypeName())) {
throw new BadRequestException("Dependency type '" + request.getTypeName() + "' already exists");
}
type.setTypeName(request.getTypeName());
}
if (request.getDescription() != null) {
type.setDescription(request.getDescription());
}
type = dependencyTypeRepository.save(type);
return mapToResponse(type);
}
public DependencyTypeResponse findById(UUID id) {
DependencyType type = dependencyTypeRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Dependency type not found with id: " + id));
return mapToResponse(type);
}
public List<DependencyTypeResponse> findAll() {
return dependencyTypeRepository.findAll().stream()
.map(this::mapToResponse)
.collect(Collectors.toList());
}
@Transactional
public void delete(UUID id) {
DependencyType type = dependencyTypeRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Dependency type not found with id: " + id));
// Check if type is being used
long count = externalDependencyRepository.countByDependencyTypeId(id);
if (count > 0) {
throw new BadRequestException("Cannot delete dependency type with " + count + " existing dependencies");
}
dependencyTypeRepository.delete(type);
}
private DependencyTypeResponse mapToResponse(DependencyType type) {
return new DependencyTypeResponse(
type.getId(),
type.getTypeName(),
type.getDescription(),
type.getIsCustom(),
type.getCreatedAt(),
type.getUpdatedAt()
);
}
}
@@ -0,0 +1,241 @@
package com.ldpv2.service;
import com.ldpv2.domain.entity.Application;
import com.ldpv2.domain.entity.DependencyType;
import com.ldpv2.domain.entity.ExternalDependency;
import com.ldpv2.dto.request.CreateExternalDependencyRequest;
import com.ldpv2.dto.request.UpdateExternalDependencyRequest;
import com.ldpv2.dto.response.ApplicationSummaryResponse;
import com.ldpv2.dto.response.DependencyTypeResponse;
import com.ldpv2.dto.response.ExternalDependencyResponse;
import com.ldpv2.exception.BadRequestException;
import com.ldpv2.exception.ResourceNotFoundException;
import com.ldpv2.repository.ApplicationRepository;
import com.ldpv2.repository.DependencyTypeRepository;
import com.ldpv2.repository.ExternalDependencyRepository;
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.time.temporal.ChronoUnit;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class ExternalDependencyService {
@Autowired
private ExternalDependencyRepository externalDependencyRepository;
@Autowired
private ApplicationRepository applicationRepository;
@Autowired
private DependencyTypeRepository dependencyTypeRepository;
@Transactional
public ExternalDependencyResponse create(UUID applicationId, CreateExternalDependencyRequest request) {
Application application = applicationRepository.findById(applicationId)
.orElseThrow(() -> new ResourceNotFoundException("Application not found with id: " + applicationId));
DependencyType dependencyType = dependencyTypeRepository.findById(request.getDependencyTypeId())
.orElseThrow(() -> new ResourceNotFoundException(
"Dependency type not found with id: " + request.getDependencyTypeId()));
// Validate dates
if (request.getValidityStartDate() != null && request.getValidityEndDate() != null) {
if (request.getValidityEndDate().isBefore(request.getValidityStartDate())) {
throw new BadRequestException("End date must be after or equal to start date");
}
}
ExternalDependency dependency = new ExternalDependency();
dependency.setApplication(application);
dependency.setDependencyType(dependencyType);
dependency.setName(request.getName());
dependency.setDescription(request.getDescription());
dependency.setTechnicalDocumentation(request.getTechnicalDocumentation());
dependency.setValidityStartDate(request.getValidityStartDate());
dependency.setValidityEndDate(request.getValidityEndDate());
dependency = externalDependencyRepository.save(dependency);
return mapToResponse(dependency);
}
@Transactional
public ExternalDependencyResponse update(UUID id, UpdateExternalDependencyRequest request) {
ExternalDependency dependency = externalDependencyRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("External dependency not found with id: " + id));
if (request.getDependencyTypeId() != null) {
DependencyType dependencyType = dependencyTypeRepository.findById(request.getDependencyTypeId())
.orElseThrow(() -> new ResourceNotFoundException(
"Dependency type not found with id: " + request.getDependencyTypeId()));
dependency.setDependencyType(dependencyType);
}
if (request.getName() != null) {
dependency.setName(request.getName());
}
if (request.getDescription() != null) {
dependency.setDescription(request.getDescription());
}
if (request.getTechnicalDocumentation() != null) {
dependency.setTechnicalDocumentation(request.getTechnicalDocumentation());
}
if (request.getValidityStartDate() != null) {
dependency.setValidityStartDate(request.getValidityStartDate());
}
if (request.getValidityEndDate() != null) {
dependency.setValidityEndDate(request.getValidityEndDate());
}
// Validate dates after updates
if (dependency.getValidityStartDate() != null && dependency.getValidityEndDate() != null) {
if (dependency.getValidityEndDate().isBefore(dependency.getValidityStartDate())) {
throw new BadRequestException("End date must be after or equal to start date");
}
}
dependency = externalDependencyRepository.save(dependency);
return mapToResponse(dependency);
}
public ExternalDependencyResponse findById(UUID id) {
ExternalDependency dependency = externalDependencyRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("External dependency not found with id: " + id));
return mapToResponse(dependency);
}
public Page<ExternalDependencyResponse> findByApplication(UUID applicationId, Pageable pageable) {
if (!applicationRepository.existsById(applicationId)) {
throw new ResourceNotFoundException("Application not found with id: " + applicationId);
}
return externalDependencyRepository.findByApplicationId(applicationId, pageable)
.map(this::mapToResponse);
}
public Page<ExternalDependencyResponse> findAll(Pageable pageable) {
return externalDependencyRepository.findAll(pageable).map(this::mapToResponse);
}
public Page<ExternalDependencyResponse> search(
UUID applicationId,
UUID dependencyTypeId,
String status,
Pageable pageable) {
LocalDate now = LocalDate.now();
LocalDate expiringDate = now.plusDays(30);
return externalDependencyRepository.search(
applicationId, dependencyTypeId, status, now, expiringDate, pageable)
.map(this::mapToResponse);
}
public List<ExternalDependencyResponse> findExpiring(int days) {
LocalDate now = LocalDate.now();
LocalDate expirationDate = now.plusDays(days);
return externalDependencyRepository.findExpiring(now, expirationDate).stream()
.map(this::mapToResponse)
.collect(Collectors.toList());
}
public List<ExternalDependencyResponse> findExpired() {
LocalDate now = LocalDate.now();
return externalDependencyRepository.findExpired(now).stream()
.map(this::mapToResponse)
.collect(Collectors.toList());
}
@Transactional
public void delete(UUID id) {
if (!externalDependencyRepository.existsById(id)) {
throw new ResourceNotFoundException("External dependency not found with id: " + id);
}
externalDependencyRepository.deleteById(id);
}
private ExternalDependencyResponse mapToResponse(ExternalDependency dependency) {
ApplicationSummaryResponse appSummary = new ApplicationSummaryResponse(
dependency.getApplication().getId(),
dependency.getApplication().getName(),
dependency.getApplication().getStatus(),
dependency.getApplication().getBusinessUnit().getName()
);
DependencyTypeResponse typeResponse = new DependencyTypeResponse(
dependency.getDependencyType().getId(),
dependency.getDependencyType().getTypeName(),
dependency.getDependencyType().getDescription(),
dependency.getDependencyType().getIsCustom(),
dependency.getDependencyType().getCreatedAt(),
dependency.getDependencyType().getUpdatedAt()
);
// Compute status
String status = computeStatus(dependency);
Boolean isActive = "ACTIVE".equals(status) || "EXPIRING".equals(status);
Integer daysUntilExpiration = computeDaysUntilExpiration(dependency);
return new ExternalDependencyResponse(
dependency.getId(),
appSummary,
typeResponse,
dependency.getName(),
dependency.getDescription(),
dependency.getTechnicalDocumentation(),
dependency.getValidityStartDate(),
dependency.getValidityEndDate(),
isActive,
daysUntilExpiration,
status,
dependency.getCreatedAt(),
dependency.getUpdatedAt()
);
}
private String computeStatus(ExternalDependency dependency) {
LocalDate now = LocalDate.now();
if (dependency.getValidityStartDate() != null && now.isBefore(dependency.getValidityStartDate())) {
return "NOT_YET_VALID";
}
if (dependency.getValidityEndDate() == null) {
return "ACTIVE"; // No end date = indefinite
}
if (now.isAfter(dependency.getValidityEndDate())) {
return "EXPIRED";
}
long daysUntilExpiration = ChronoUnit.DAYS.between(now, dependency.getValidityEndDate());
if (daysUntilExpiration <= 30) {
return "EXPIRING";
}
return "ACTIVE";
}
private Integer computeDaysUntilExpiration(ExternalDependency dependency) {
if (dependency.getValidityEndDate() == null) {
return null;
}
LocalDate now = LocalDate.now();
if (now.isAfter(dependency.getValidityEndDate())) {
return null; // Already expired
}
return (int) ChronoUnit.DAYS.between(now, dependency.getValidityEndDate());
}
}
@@ -14,5 +14,6 @@
<include file="db/changelog/v1.0/006-create-version-table.xml"/> <include file="db/changelog/v1.0/006-create-version-table.xml"/>
<include file="db/changelog/v1.0/007-create-deployment-table.xml"/> <include file="db/changelog/v1.0/007-create-deployment-table.xml"/>
<include file="db/changelog/v1.0/008-create-application-contact-table.xml"/> <include file="db/changelog/v1.0/008-create-application-contact-table.xml"/>
<include file="db/changelog/v1.0/009-create-external-dependency-tables.xml"/>
</databaseChangeLog> </databaseChangeLog>
@@ -0,0 +1,137 @@
<?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="009-create-external-dependency-tables" author="ldpv2-team">
<!-- Dependency Types Catalog -->
<createTable tableName="dependency_type">
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="type_name" type="VARCHAR(100)">
<constraints nullable="false" unique="true"/>
</column>
<column name="description" type="TEXT"/>
<column name="is_custom" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<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>
<!-- External Dependencies -->
<createTable tableName="external_dependency">
<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_ext_dep_application"
references="application(id)"
deleteCascade="true"/>
</column>
<column name="dependency_type_id" type="UUID">
<constraints nullable="false"
foreignKeyName="fk_ext_dep_type"
references="dependency_type(id)"/>
</column>
<column name="name" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="description" type="TEXT"/>
<column name="technical_documentation" type="TEXT"/>
<column name="validity_start_date" type="DATE"/>
<column name="validity_end_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>
<!-- Add check constraint for validity dates -->
<sql>
ALTER TABLE external_dependency
ADD CONSTRAINT check_validity_dates
CHECK (validity_end_date IS NULL OR validity_start_date IS NULL OR validity_end_date >= validity_start_date);
</sql>
<!-- Indexes -->
<createIndex tableName="dependency_type" indexName="idx_dep_type_name">
<column name="type_name"/>
</createIndex>
<createIndex tableName="external_dependency" indexName="idx_ext_dep_application">
<column name="application_id"/>
</createIndex>
<createIndex tableName="external_dependency" indexName="idx_ext_dep_type">
<column name="dependency_type_id"/>
</createIndex>
<createIndex tableName="external_dependency" indexName="idx_ext_dep_validity_end">
<column name="validity_end_date"/>
</createIndex>
<createIndex tableName="external_dependency" indexName="idx_ext_dep_name">
<column name="name"/>
</createIndex>
<!-- Insert default dependency types -->
<insert tableName="dependency_type">
<column name="type_name" value="WEB_SERVICE"/>
<column name="description" value="REST APIs, SOAP services, microservices"/>
<column name="is_custom" valueBoolean="false"/>
</insert>
<insert tableName="dependency_type">
<column name="type_name" value="DATABASE"/>
<column name="description" value="External database connections"/>
<column name="is_custom" valueBoolean="false"/>
</insert>
<insert tableName="dependency_type">
<column name="type_name" value="CERTIFICATE"/>
<column name="description" value="SSL/TLS certificates, authentication certificates"/>
<column name="is_custom" valueBoolean="false"/>
</insert>
<insert tableName="dependency_type">
<column name="type_name" value="NETWORK_FLOW"/>
<column name="description" value="Network connections, firewall rules, VPN tunnels"/>
<column name="is_custom" valueBoolean="false"/>
</insert>
<!-- Sample data -->
<insert tableName="external_dependency">
<column name="application_id" valueComputed="(SELECT id FROM application WHERE name = 'Customer Portal' LIMIT 1)"/>
<column name="dependency_type_id" valueComputed="(SELECT id FROM dependency_type WHERE type_name = 'WEB_SERVICE' LIMIT 1)"/>
<column name="name" value="Payment Gateway API"/>
<column name="description" value="External payment processing service"/>
<column name="technical_documentation" value="Endpoint: https://api.payment.example.com/v2"/>
<column name="validity_start_date" value="2024-01-01"/>
<column name="validity_end_date" value="2027-12-31"/>
</insert>
<insert tableName="external_dependency">
<column name="application_id" valueComputed="(SELECT id FROM application WHERE name = 'Internal CRM' LIMIT 1)"/>
<column name="dependency_type_id" valueComputed="(SELECT id FROM dependency_type WHERE type_name = 'DATABASE' LIMIT 1)"/>
<column name="name" value="Legacy Customer Database"/>
<column name="description" value="Read-only connection to legacy system"/>
<column name="technical_documentation" value="Server: legacy-db.internal:5432"/>
<column name="validity_start_date" value="2020-01-01"/>
<column name="validity_end_date" value="2026-06-30"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,81 @@
<div class="dependencies-container">
<div class="header">
<h3>External Dependencies</h3>
<button (click)="createNew()" class="btn-primary">Add Dependency</button>
</div>
<div class="filters">
<div class="filter-group">
<label>Filter by Type:</label>
<select [(ngModel)]="selectedTypeId" (ngModelChange)="onFilterChange()" class="filter-select">
<option value="">All Types</option>
<option *ngFor="let type of dependencyTypes" [value]="type.id">
{{ type.typeName }}
</option>
</select>
</div>
<div class="filter-group">
<label>Filter by Status:</label>
<select [(ngModel)]="selectedStatus" (ngModelChange)="onFilterChange()" class="filter-select">
<option *ngFor="let opt of statusOptions" [value]="opt.value">
{{ opt.label }}
</option>
</select>
</div>
</div>
<div *ngIf="loading" class="loading">Loading dependencies...</div>
<div *ngIf="error" class="error">{{ error }}</div>
<div *ngIf="!loading && dependencies.length > 0" class="dependencies-table">
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Status</th>
<th>Validity Period</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let dep of dependencies">
<td><strong>{{ dep.name }}</strong></td>
<td>{{ dep.dependencyType.typeName }}</td>
<td>
<span class="status-badge" [ngClass]="getStatusClass(dep.status)">
{{ getStatusLabel(dep.status) }}
<span *ngIf="dep.daysUntilExpiration !== null && dep.daysUntilExpiration !== undefined">
({{ dep.daysUntilExpiration }} days)
</span>
</span>
</td>
<td>
<div *ngIf="dep.validityStartDate || dep.validityEndDate">
{{ dep.validityStartDate ? (dep.validityStartDate | date:'mediumDate') : '-' }}
{{ dep.validityEndDate ? (dep.validityEndDate | date:'mediumDate') : 'Indefinite' }}
</div>
<div *ngIf="!dep.validityStartDate && !dep.validityEndDate">-</div>
</td>
<td class="actions">
<button (click)="viewDetails(dep.id)" class="btn-sm">View</button>
<button (click)="edit(dep.id)" class="btn-sm">Edit</button>
<button (click)="delete(dep.id)" class="btn-sm btn-danger">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
<div *ngIf="!loading && dependencies.length === 0" class="empty">
No external dependencies found for this application.
</div>
<div *ngIf="totalPages > 1" class="pagination">
<button (click)="previousPage()" [disabled]="page === 0">Previous</button>
<span>Page {{ page + 1 }} of {{ totalPages }}</span>
<button (click)="nextPage()" [disabled]="page >= totalPages - 1">Next</button>
</div>
</div>
@@ -0,0 +1,181 @@
.dependencies-container {
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
h3 {
margin: 0;
}
}
}
.btn-primary {
background-color: #3f51b5;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: #303f9f;
}
}
.filters {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
padding: 1rem;
background: #f9f9f9;
border-radius: 8px;
.filter-group {
flex: 1;
min-width: 200px;
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #555;
}
.filter-select {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.95rem;
&:focus {
outline: none;
border-color: #3f51b5;
}
}
}
}
.loading, .error, .empty {
text-align: center;
padding: 2rem;
color: #666;
}
.error {
color: #f44336;
}
.dependencies-table {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
table {
width: 100%;
border-collapse: collapse;
th, td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #f5f5f5;
}
th {
background-color: #f9f9f9;
font-weight: 600;
color: #555;
}
tbody tr:hover {
background-color: #fafafa;
}
}
}
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.875rem;
font-weight: 500;
display: inline-block;
&.status-active {
background-color: #e8f5e9;
color: #388e3c;
}
&.status-expiring {
background-color: #fff3e0;
color: #f57c00;
}
&.status-expired {
background-color: #ffebee;
color: #c62828;
}
&.status-not-valid {
background-color: #f5f5f5;
color: #616161;
}
}
.actions {
display: flex;
gap: 0.5rem;
}
.btn-sm {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
background-color: #2196f3;
color: white;
font-size: 0.875rem;
&:hover {
background-color: #1976d2;
}
&.btn-danger {
background-color: #f44336;
&:hover {
background-color: #d32f2f;
}
}
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-top: 1.5rem;
button {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
&:hover:not(:disabled) {
background-color: #f5f5f5;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
span {
color: #666;
}
}
@@ -0,0 +1,152 @@
import { Component, Input, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { DependencyService } from '../../dependencies/dependency.service';
import { ExternalDependency, DependencyType } from '../../../shared/models/dependency.model';
import { Page } from '../../../shared/models/environment.model';
@Component({
selector: 'app-application-dependencies',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './application-dependencies.component.html',
styleUrls: ['./application-dependencies.component.scss']
})
export class ApplicationDependenciesComponent implements OnInit {
@Input() applicationId!: string;
@Input() applicationName!: string;
dependencies: ExternalDependency[] = [];
dependencyTypes: DependencyType[] = [];
loading = false;
error = '';
page = 0;
size = 10;
totalPages = 0;
selectedTypeId = '';
selectedStatus = '';
statusOptions = [
{ value: '', label: 'All Statuses' },
{ value: 'ACTIVE', label: 'Active' },
{ value: 'EXPIRING', label: 'Expiring Soon' },
{ value: 'EXPIRED', label: 'Expired' },
{ value: 'NOT_YET_VALID', label: 'Not Yet Valid' }
];
constructor(
private dependencyService: DependencyService,
private router: Router
) {}
ngOnInit(): void {
if (this.applicationId) {
this.loadDependencyTypes();
this.loadDependencies();
}
}
loadDependencyTypes(): void {
this.dependencyService.getDependencyTypes().subscribe({
next: (types) => {
this.dependencyTypes = types;
},
error: (err) => {
console.error('Failed to load dependency types', err);
}
});
}
loadDependencies(): void {
this.loading = true;
const filters: any = { applicationId: this.applicationId };
if (this.selectedTypeId) {
filters.dependencyTypeId = this.selectedTypeId;
}
if (this.selectedStatus) {
filters.status = this.selectedStatus;
}
this.dependencyService.getDependencies(filters, this.page, this.size).subscribe({
next: (data: Page<ExternalDependency>) => {
this.dependencies = data.content;
this.totalPages = data.totalPages;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load dependencies';
this.loading = false;
}
});
}
onFilterChange(): void {
this.page = 0;
this.loadDependencies();
}
createNew(): void {
this.router.navigate(['/dependencies/new'], {
queryParams: { applicationId: this.applicationId }
});
}
viewDetails(id: string): void {
this.router.navigate(['/dependencies', id]);
}
edit(id: string): void {
this.router.navigate(['/dependencies', id, 'edit']);
}
delete(id: string): void {
if (confirm('Are you sure you want to delete this dependency?')) {
this.dependencyService.deleteDependency(id).subscribe({
next: () => {
this.loadDependencies();
},
error: (err) => {
this.error = 'Failed to delete dependency';
}
});
}
}
getStatusClass(status: string): string {
const classes: Record<string, string> = {
'ACTIVE': 'status-active',
'EXPIRING': 'status-expiring',
'EXPIRED': 'status-expired',
'NOT_YET_VALID': 'status-not-valid'
};
return classes[status] || '';
}
getStatusLabel(status: string): string {
const labels: Record<string, string> = {
'ACTIVE': 'Active',
'EXPIRING': 'Expiring Soon',
'EXPIRED': 'Expired',
'NOT_YET_VALID': 'Not Yet Valid'
};
return labels[status] || status;
}
nextPage(): void {
if (this.page < this.totalPages - 1) {
this.page++;
this.loadDependencies();
}
}
previousPage(): void {
if (this.page > 0) {
this.page--;
this.loadDependencies();
}
}
}
@@ -0,0 +1,123 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import {
ExternalDependency,
CreateExternalDependencyRequest,
UpdateExternalDependencyRequest,
DependencyType,
CreateDependencyTypeRequest,
UpdateDependencyTypeRequest
} from '../../shared/models/dependency.model';
import { Page } from '../../shared/models/environment.model';
@Injectable({
providedIn: 'root'
})
export class DependencyService {
private readonly API_URL = '/api/dependencies';
private readonly TYPE_API_URL = '/api/dependency-types';
constructor(private http: HttpClient) {}
// Dependency Types
getDependencyTypes(): Observable<DependencyType[]> {
return this.http.get<DependencyType[]>(this.TYPE_API_URL);
}
getDependencyType(id: string): Observable<DependencyType> {
return this.http.get<DependencyType>(`${this.TYPE_API_URL}/${id}`);
}
createDependencyType(data: CreateDependencyTypeRequest): Observable<DependencyType> {
return this.http.post<DependencyType>(this.TYPE_API_URL, data);
}
updateDependencyType(id: string, data: UpdateDependencyTypeRequest): Observable<DependencyType> {
return this.http.put<DependencyType>(`${this.TYPE_API_URL}/${id}`, data);
}
deleteDependencyType(id: string): Observable<void> {
return this.http.delete<void>(`${this.TYPE_API_URL}/${id}`);
}
// External Dependencies
getDependencies(
filters?: {
applicationId?: string;
dependencyTypeId?: string;
status?: string;
},
page: number = 0,
size: number = 20,
sortBy: string = 'name',
sortDirection: string = 'asc'
): Observable<Page<ExternalDependency>> {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString())
.set('sortBy', sortBy)
.set('sortDirection', sortDirection);
if (filters?.applicationId) {
params = params.set('applicationId', filters.applicationId);
}
if (filters?.dependencyTypeId) {
params = params.set('dependencyTypeId', filters.dependencyTypeId);
}
if (filters?.status) {
params = params.set('status', filters.status);
}
return this.http.get<Page<ExternalDependency>>(this.API_URL, { params });
}
getDependency(id: string): Observable<ExternalDependency> {
return this.http.get<ExternalDependency>(`${this.API_URL}/${id}`);
}
getDependenciesByApplication(
applicationId: string,
page: number = 0,
size: number = 20
): Observable<Page<ExternalDependency>> {
const params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get<Page<ExternalDependency>>(
`${this.API_URL}/by-application/${applicationId}`,
{ params }
);
}
createDependency(
applicationId: string,
data: CreateExternalDependencyRequest
): Observable<ExternalDependency> {
return this.http.post<ExternalDependency>(
`${this.API_URL}/for-application/${applicationId}`,
data
);
}
updateDependency(
id: string,
data: UpdateExternalDependencyRequest
): Observable<ExternalDependency> {
return this.http.put<ExternalDependency>(`${this.API_URL}/${id}`, data);
}
deleteDependency(id: string): Observable<void> {
return this.http.delete<void>(`${this.API_URL}/${id}`);
}
getExpiringDependencies(days: number = 30): Observable<ExternalDependency[]> {
const params = new HttpParams().set('days', days.toString());
return this.http.get<ExternalDependency[]>(`${this.API_URL}/expiring`, { params });
}
getExpiredDependencies(): Observable<ExternalDependency[]> {
return this.http.get<ExternalDependency[]>(`${this.API_URL}/expired`);
}
}
@@ -0,0 +1,55 @@
export interface DependencyType {
id: string;
typeName: string;
description?: string;
isCustom: boolean;
createdAt: Date;
updatedAt: Date;
}
export interface ExternalDependency {
id: string;
application: {
id: string;
name: string;
};
dependencyType: DependencyType;
name: string;
description?: string;
technicalDocumentation?: string;
validityStartDate?: Date;
validityEndDate?: Date;
isActive: boolean;
daysUntilExpiration?: number;
status: 'ACTIVE' | 'EXPIRING' | 'EXPIRED' | 'NOT_YET_VALID';
createdAt: Date;
updatedAt: Date;
}
export interface CreateDependencyTypeRequest {
typeName: string;
description?: string;
}
export interface UpdateDependencyTypeRequest {
typeName?: string;
description?: string;
}
export interface CreateExternalDependencyRequest {
dependencyTypeId: string;
name: string;
description?: string;
technicalDocumentation?: string;
validityStartDate?: Date;
validityEndDate?: Date;
}
export interface UpdateExternalDependencyRequest {
dependencyTypeId?: string;
name?: string;
description?: string;
technicalDocumentation?: string;
validityStartDate?: Date;
validityEndDate?: Date;
}