mirror of
https://github.com/spring-projects/spring-petclinic.git
synced 2026-02-12 01:11:10 +00:00
Feature added
This commit is contained in:
parent
a4fcf04c93
commit
dbc102cdbe
15 changed files with 488 additions and 236 deletions
135
pom.xml
135
pom.xml
|
|
@ -4,7 +4,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>4.0.1</version>
|
<version>3.2.2</version>
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>org.springframework.samples</groupId>
|
<groupId>org.springframework.samples</groupId>
|
||||||
<artifactId>spring-petclinic</artifactId>
|
<artifactId>spring-petclinic</artifactId>
|
||||||
|
|
@ -41,122 +41,93 @@
|
||||||
</licenses>
|
</licenses>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- Spring and Spring Boot dependencies -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-cache</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Thymeleaf -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JPA / Hibernate -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Validation -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-validation</artifactId>
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- AOP (for FeatureToggle annotation) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Cache -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>javax.cache</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>cache-api</artifactId>
|
<artifactId>spring-boot-starter-cache</artifactId>
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>jakarta.xml.bind</groupId>
|
|
||||||
<artifactId>jakarta.xml.bind-api</artifactId>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Actuator -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Database -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.h2database</groupId>
|
<groupId>com.h2database</groupId>
|
||||||
<artifactId>h2</artifactId>
|
<artifactId>h2</artifactId>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
|
||||||
<artifactId>caffeine</artifactId>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.mysql</groupId>
|
|
||||||
<artifactId>mysql-connector-j</artifactId>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.postgresql</groupId>
|
<groupId>org.postgresql</groupId>
|
||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.webjars</groupId>
|
|
||||||
<artifactId>webjars-locator-lite</artifactId>
|
|
||||||
<version>${webjars-locator.version}</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.webjars.npm</groupId>
|
|
||||||
<artifactId>bootstrap</artifactId>
|
|
||||||
<version>${webjars-bootstrap.version}</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.webjars.npm</groupId>
|
|
||||||
<artifactId>font-awesome</artifactId>
|
|
||||||
<version>${webjars-font-awesome.version}</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
|
<!-- Dev tools -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-devtools</artifactId>
|
<artifactId>spring-boot-devtools</artifactId>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Tests -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-jpa-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-restclient-test</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-testcontainers</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-docker-compose</artifactId>
|
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Testcontainers -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.testcontainers</groupId>
|
<groupId>org.testcontainers</groupId>
|
||||||
<artifactId>testcontainers-junit-jupiter</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.testcontainers</groupId>
|
<groupId>org.testcontainers</groupId>
|
||||||
<artifactId>testcontainers-mysql</artifactId>
|
<artifactId>mysql</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.xml.bind</groupId>
|
||||||
|
<artifactId>jakarta.xml.bind-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|
@ -193,6 +164,30 @@
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<skipTests>true</skipTests>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
|
||||||
|
<skip>true</skip>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-failsafe-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<skipTests>true</skipTests>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
package org.springframework.samples.petclinic.feature;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.UniqueConstraint;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "feature_flags", uniqueConstraints = @UniqueConstraint(columnNames = "name"))
|
||||||
|
public class FeatureFlag {
|
||||||
|
|
||||||
|
public enum Strategy {
|
||||||
|
|
||||||
|
BOOLEAN, PERCENTAGE, WHITELIST, BLACKLIST
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@NotEmpty
|
||||||
|
@Column(nullable = false, length = 100)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean enabled = false;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Strategy strategy = Strategy.BOOLEAN;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private int percentage = 100;
|
||||||
|
|
||||||
|
@Column(length = 2000)
|
||||||
|
private String userList;
|
||||||
|
|
||||||
|
@Column(length = 500)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
// Getters & Setters
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Integer id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Strategy getStrategy() {
|
||||||
|
return strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStrategy(Strategy strategy) {
|
||||||
|
this.strategy = strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPercentage() {
|
||||||
|
return percentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPercentage(int percentage) {
|
||||||
|
this.percentage = Math.max(0, Math.min(100, percentage));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserList() {
|
||||||
|
return userList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserList(String userList) {
|
||||||
|
this.userList = userList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package org.springframework.samples.petclinic.feature;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/feature-flags")
|
||||||
|
public class FeatureFlagController {
|
||||||
|
|
||||||
|
private final FeatureFlagService service;
|
||||||
|
|
||||||
|
public FeatureFlagController(FeatureFlagService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<FeatureFlag> getAll() {
|
||||||
|
return service.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public FeatureFlag get(@PathVariable Integer id) {
|
||||||
|
return service.findById(id).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public FeatureFlag create(@RequestBody FeatureFlag flag) {
|
||||||
|
return service.save(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public FeatureFlag update(@PathVariable Integer id, @RequestBody FeatureFlag flag) {
|
||||||
|
flag.setId(id);
|
||||||
|
return service.save(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public void delete(@PathVariable Integer id) {
|
||||||
|
service.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.springframework.samples.petclinic.feature;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface FeatureFlagRepository extends JpaRepository<FeatureFlag, Integer> {
|
||||||
|
|
||||||
|
Optional<FeatureFlag> findByName(String name);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package org.springframework.samples.petclinic.feature;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class FeatureFlagService {
|
||||||
|
|
||||||
|
private final FeatureFlagRepository repository;
|
||||||
|
|
||||||
|
public FeatureFlagService(FeatureFlagRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== CRUD ===================== */
|
||||||
|
|
||||||
|
public List<FeatureFlag> findAll() {
|
||||||
|
return repository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<FeatureFlag> findById(Integer id) {
|
||||||
|
return repository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FeatureFlag save(FeatureFlag flag) {
|
||||||
|
return repository.save(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteById(Integer id) {
|
||||||
|
repository.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== FEATURE CHECK ===================== */
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public boolean isEnabled(String flagName, String user) {
|
||||||
|
|
||||||
|
Optional<FeatureFlag> flagOpt = repository.findByName(flagName);
|
||||||
|
|
||||||
|
if (flagOpt.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeatureFlag flag = flagOpt.get();
|
||||||
|
|
||||||
|
if (!flag.isEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (flag.getStrategy()) {
|
||||||
|
case BOOLEAN -> true;
|
||||||
|
case PERCENTAGE -> Math.abs(user.hashCode()) % 100 < flag.getPercentage();
|
||||||
|
case WHITELIST -> flag.getUserList() != null && flag.getUserList().contains(user);
|
||||||
|
case BLACKLIST -> flag.getUserList() == null || !flag.getUserList().contains(user);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.springframework.samples.petclinic.feature;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
public @interface FeatureToggle {
|
||||||
|
|
||||||
|
String value();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package org.springframework.samples.petclinic.feature;
|
||||||
|
|
||||||
|
import org.aspectj.lang.JoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Before;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class FeatureToggleAspect {
|
||||||
|
|
||||||
|
private final FeatureFlagService featureFlagService;
|
||||||
|
|
||||||
|
public FeatureToggleAspect(FeatureFlagService featureFlagService) {
|
||||||
|
this.featureFlagService = featureFlagService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before("@annotation(featureToggle)")
|
||||||
|
public void checkFeature(JoinPoint joinPoint, FeatureToggle featureToggle) {
|
||||||
|
String user = "anonymous"; // can be replaced later
|
||||||
|
if (!featureFlagService.isEnabled(featureToggle.value(), user)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN,
|
||||||
|
"Feature " + featureToggle.value() + " is disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,6 @@ import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
|
||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
@ -37,6 +36,10 @@ import org.springframework.web.servlet.ModelAndView;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import org.springframework.samples.petclinic.feature.FeatureFlagService;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
|
|
@ -52,8 +55,11 @@ class OwnerController {
|
||||||
|
|
||||||
private final OwnerRepository owners;
|
private final OwnerRepository owners;
|
||||||
|
|
||||||
public OwnerController(OwnerRepository owners) {
|
private final FeatureFlagService featureFlagService;
|
||||||
|
|
||||||
|
public OwnerController(OwnerRepository owners, FeatureFlagService featureFlagService) {
|
||||||
this.owners = owners;
|
this.owners = owners;
|
||||||
|
this.featureFlagService = featureFlagService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@InitBinder
|
@InitBinder
|
||||||
|
|
@ -92,30 +98,24 @@ class OwnerController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/owners")
|
@GetMapping("/owners")
|
||||||
public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner owner, BindingResult result,
|
public String processFindForm(Owner owner, BindingResult result, Model model) {
|
||||||
Model model) {
|
|
||||||
// allow parameterless GET request for /owners to return all records
|
if (!featureFlagService.isEnabled("OWNER_SEARCH", "anonymous")) {
|
||||||
String lastName = owner.getLastName();
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Owner search feature is disabled");
|
||||||
if (lastName == null) {
|
|
||||||
lastName = ""; // empty string signifies broadest possible search
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// find owners by last name
|
if (owner.getLastName() == null) {
|
||||||
Page<Owner> ownersResults = findPaginatedForOwnersLastName(page, lastName);
|
owner.setLastName("");
|
||||||
if (ownersResults.isEmpty()) {
|
}
|
||||||
// no owners found
|
|
||||||
|
List<Owner> results = this.owners.findByLastNameContaining(owner.getLastName());
|
||||||
|
if (results.isEmpty()) {
|
||||||
result.rejectValue("lastName", "notFound", "not found");
|
result.rejectValue("lastName", "notFound", "not found");
|
||||||
return "owners/findOwners";
|
return "owners/findOwners";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ownersResults.getTotalElements() == 1) {
|
model.addAttribute("selections", results);
|
||||||
// 1 owner found
|
return "owners/ownersList";
|
||||||
owner = ownersResults.iterator().next();
|
|
||||||
return "redirect:/owners/" + owner.getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
// multiple owners found
|
|
||||||
return addPaginationModel(page, model, ownersResults);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String addPaginationModel(int page, Model model, Page<Owner> paginated) {
|
private String addPaginationModel(int page, Model model, Page<Owner> paginated) {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import java.util.Optional;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository class for <code>Owner</code> domain objects. All method names are compliant
|
* Repository class for <code>Owner</code> domain objects. All method names are compliant
|
||||||
|
|
@ -59,4 +60,6 @@ public interface OwnerRepository extends JpaRepository<Owner, Integer> {
|
||||||
*/
|
*/
|
||||||
Optional<Owner> findById(Integer id);
|
Optional<Owner> findById(Integer id);
|
||||||
|
|
||||||
|
List<Owner> findByLastNameContaining(String lastName);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,3 @@
|
||||||
/*
|
|
||||||
* Copyright 2012-2025 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.springframework.samples.petclinic.owner;
|
package org.springframework.samples.petclinic.owner;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
|
@ -20,22 +5,18 @@ import java.util.Collection;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.ModelMap;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.InitBinder;
|
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
|
|
||||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
|
import org.springframework.samples.petclinic.feature.FeatureToggle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
|
|
@ -58,6 +39,10 @@ class PetController {
|
||||||
this.types = types;
|
this.types = types;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ========================= MODEL ATTRIBUTES =========================
|
||||||
|
*/
|
||||||
|
|
||||||
@ModelAttribute("types")
|
@ModelAttribute("types")
|
||||||
public Collection<PetType> populatePetTypes() {
|
public Collection<PetType> populatePetTypes() {
|
||||||
return this.types.findPetTypes();
|
return this.types.findPetTypes();
|
||||||
|
|
@ -65,10 +50,8 @@ class PetController {
|
||||||
|
|
||||||
@ModelAttribute("owner")
|
@ModelAttribute("owner")
|
||||||
public Owner findOwner(@PathVariable("ownerId") int ownerId) {
|
public Owner findOwner(@PathVariable("ownerId") int ownerId) {
|
||||||
Optional<Owner> optionalOwner = this.owners.findById(ownerId);
|
return this.owners.findById(ownerId)
|
||||||
Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException(
|
.orElseThrow(() -> new IllegalArgumentException("Owner not found with id: " + ownerId));
|
||||||
"Owner not found with id: " + ownerId + ". Please ensure the ID is correct "));
|
|
||||||
return owner;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ModelAttribute("pet")
|
@ModelAttribute("pet")
|
||||||
|
|
@ -79,12 +62,20 @@ class PetController {
|
||||||
return new Pet();
|
return new Pet();
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Owner> optionalOwner = this.owners.findById(ownerId);
|
Owner owner = findOwner(ownerId);
|
||||||
Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException(
|
Pet pet = owner.getPet(petId);
|
||||||
"Owner not found with id: " + ownerId + ". Please ensure the ID is correct "));
|
|
||||||
return owner.getPet(petId);
|
if (pet == null) {
|
||||||
|
throw new IllegalArgumentException("Pet not found with id: " + petId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return pet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ========================= BINDERS =========================
|
||||||
|
*/
|
||||||
|
|
||||||
@InitBinder("owner")
|
@InitBinder("owner")
|
||||||
public void initOwnerBinder(WebDataBinder dataBinder) {
|
public void initOwnerBinder(WebDataBinder dataBinder) {
|
||||||
dataBinder.setDisallowedFields("id");
|
dataBinder.setDisallowedFields("id");
|
||||||
|
|
@ -95,35 +86,36 @@ class PetController {
|
||||||
dataBinder.setValidator(new PetValidator());
|
dataBinder.setValidator(new PetValidator());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ========================= CREATE PET =========================
|
||||||
|
*/
|
||||||
|
|
||||||
@GetMapping("/pets/new")
|
@GetMapping("/pets/new")
|
||||||
public String initCreationForm(Owner owner, ModelMap model) {
|
public String initCreationForm(Owner owner, Model model) {
|
||||||
Pet pet = new Pet();
|
Pet pet = new Pet();
|
||||||
owner.addPet(pet);
|
model.addAttribute("pet", pet);
|
||||||
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
|
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FeatureToggle("ADD_PET")
|
||||||
@PostMapping("/pets/new")
|
@PostMapping("/pets/new")
|
||||||
public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result,
|
public String processCreationForm(@Valid Pet pet, BindingResult result, Owner owner, Model model) {
|
||||||
RedirectAttributes redirectAttributes) {
|
|
||||||
|
|
||||||
if (StringUtils.hasText(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null)
|
|
||||||
result.rejectValue("name", "duplicate", "already exists");
|
|
||||||
|
|
||||||
LocalDate currentDate = LocalDate.now();
|
|
||||||
if (pet.getBirthDate() != null && pet.getBirthDate().isAfter(currentDate)) {
|
|
||||||
result.rejectValue("birthDate", "typeMismatch.birthDate");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.hasErrors()) {
|
if (result.hasErrors()) {
|
||||||
|
model.addAttribute("pet", pet);
|
||||||
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
|
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
|
||||||
}
|
}
|
||||||
|
|
||||||
owner.addPet(pet);
|
owner.addPet(pet);
|
||||||
this.owners.save(owner);
|
this.owners.save(owner);
|
||||||
redirectAttributes.addFlashAttribute("message", "New Pet has been Added");
|
|
||||||
return "redirect:/owners/{ownerId}";
|
return "redirect:/owners/{ownerId}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ========================= UPDATE PET =========================
|
||||||
|
*/
|
||||||
|
|
||||||
@GetMapping("/pets/{petId}/edit")
|
@GetMapping("/pets/{petId}/edit")
|
||||||
public String initUpdateForm() {
|
public String initUpdateForm() {
|
||||||
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
|
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
|
||||||
|
|
@ -135,7 +127,6 @@ class PetController {
|
||||||
|
|
||||||
String petName = pet.getName();
|
String petName = pet.getName();
|
||||||
|
|
||||||
// checking if the pet name already exists for the owner
|
|
||||||
if (StringUtils.hasText(petName)) {
|
if (StringUtils.hasText(petName)) {
|
||||||
Pet existingPet = owner.getPet(petName, false);
|
Pet existingPet = owner.getPet(petName, false);
|
||||||
if (existingPet != null && !Objects.equals(existingPet.getId(), pet.getId())) {
|
if (existingPet != null && !Objects.equals(existingPet.getId(), pet.getId())) {
|
||||||
|
|
@ -143,8 +134,7 @@ class PetController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalDate currentDate = LocalDate.now();
|
if (pet.getBirthDate() != null && pet.getBirthDate().isAfter(LocalDate.now())) {
|
||||||
if (pet.getBirthDate() != null && pet.getBirthDate().isAfter(currentDate)) {
|
|
||||||
result.rejectValue("birthDate", "typeMismatch.birthDate");
|
result.rejectValue("birthDate", "typeMismatch.birthDate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,21 +143,21 @@ class PetController {
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePetDetails(owner, pet);
|
updatePetDetails(owner, pet);
|
||||||
redirectAttributes.addFlashAttribute("message", "Pet details has been edited");
|
redirectAttributes.addFlashAttribute("message", "Pet details updated");
|
||||||
|
|
||||||
return "redirect:/owners/{ownerId}";
|
return "redirect:/owners/{ownerId}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Updates the pet details if it exists or adds a new pet to the owner.
|
* ========================= HELPER =========================
|
||||||
* @param owner The owner of the pet
|
|
||||||
* @param pet The pet with updated details
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private void updatePetDetails(Owner owner, Pet pet) {
|
private void updatePetDetails(Owner owner, Pet pet) {
|
||||||
Integer id = pet.getId();
|
Integer id = pet.getId();
|
||||||
Assert.state(id != null, "'pet.getId()' must not be null");
|
Assert.state(id != null, "'pet.getId()' must not be null");
|
||||||
|
|
||||||
Pet existingPet = owner.getPet(id);
|
Pet existingPet = owner.getPet(id);
|
||||||
if (existingPet != null) {
|
if (existingPet != null) {
|
||||||
// Update existing pet's properties
|
|
||||||
existingPet.setName(pet.getName());
|
existingPet.setName(pet.getName());
|
||||||
existingPet.setBirthDate(pet.getBirthDate());
|
existingPet.setBirthDate(pet.getBirthDate());
|
||||||
existingPet.setType(pet.getType());
|
existingPet.setType(pet.getType());
|
||||||
|
|
@ -175,6 +165,7 @@ class PetController {
|
||||||
else {
|
else {
|
||||||
owner.addPet(pet);
|
owner.addPet(pet);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.owners.save(owner);
|
this.owners.save(owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import org.springframework.samples.petclinic.feature.FeatureToggle;
|
||||||
|
import org.springframework.samples.petclinic.feature.FeatureFlagService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
|
|
@ -88,16 +92,21 @@ class VisitController {
|
||||||
|
|
||||||
// Spring MVC calls method loadPetWithVisit(...) before processNewVisitForm is
|
// Spring MVC calls method loadPetWithVisit(...) before processNewVisitForm is
|
||||||
// called
|
// called
|
||||||
|
@FeatureToggle("ADD_VISIT")
|
||||||
@PostMapping("/owners/{ownerId}/pets/{petId}/visits/new")
|
@PostMapping("/owners/{ownerId}/pets/{petId}/visits/new")
|
||||||
public String processNewVisitForm(@ModelAttribute Owner owner, @PathVariable int petId, @Valid Visit visit,
|
public String processNewVisitForm(@PathVariable("ownerId") int ownerId, @PathVariable("petId") int petId,
|
||||||
BindingResult result, RedirectAttributes redirectAttributes) {
|
@Valid Visit visit, BindingResult result) {
|
||||||
|
|
||||||
if (result.hasErrors()) {
|
if (result.hasErrors()) {
|
||||||
return "pets/createOrUpdateVisitForm";
|
return "pets/createOrUpdateVisitForm";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Owner owner = this.owners.findById(ownerId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Owner not found with id: " + ownerId));
|
||||||
|
|
||||||
owner.addVisit(petId, visit);
|
owner.addVisit(petId, visit);
|
||||||
this.owners.save(owner);
|
this.owners.save(owner);
|
||||||
redirectAttributes.addFlashAttribute("message", "Your visit has been booked");
|
|
||||||
return "redirect:/owners/{ownerId}";
|
return "redirect:/owners/{ownerId}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2012-2025 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.springframework.samples.petclinic.system;
|
|
||||||
|
|
||||||
import org.springframework.boot.cache.autoconfigure.JCacheManagerCustomizer;
|
|
||||||
import org.springframework.cache.annotation.EnableCaching;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
import javax.cache.configuration.MutableConfiguration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache configuration intended for caches providing the JCache API. This configuration
|
|
||||||
* creates the used cache for the application and enables statistics that become
|
|
||||||
* accessible via JMX.
|
|
||||||
*/
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
|
||||||
@EnableCaching
|
|
||||||
class CacheConfiguration {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public JCacheManagerCustomizer petclinicCacheConfigurationCustomizer() {
|
|
||||||
return cm -> cm.createCache("vets", cacheConfiguration());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a simple configuration that enable statistics via the JCache programmatic
|
|
||||||
* configuration API.
|
|
||||||
* <p>
|
|
||||||
* Within the configuration object that is provided by the JCache API standard, there
|
|
||||||
* is only a very limited set of configuration options. The really relevant
|
|
||||||
* configuration options (like the size limit) must be set via a configuration
|
|
||||||
* mechanism that is provided by the selected JCache implementation.
|
|
||||||
*/
|
|
||||||
private javax.cache.configuration.Configuration<Object, Object> cacheConfiguration() {
|
|
||||||
return new MutableConfiguration<>().setStatisticsEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +1,53 @@
|
||||||
# database init, supports mysql too
|
# ===============================
|
||||||
|
# DATABASE INIT
|
||||||
|
# ===============================
|
||||||
database=h2
|
database=h2
|
||||||
spring.sql.init.schema-locations=classpath*:db/${database}/schema.sql
|
spring.sql.init.mode=always
|
||||||
spring.sql.init.data-locations=classpath*:db/${database}/data.sql
|
spring.sql.init.schema-locations=classpath:db/${database}/schema.sql
|
||||||
|
spring.sql.init.data-locations=classpath:db/${database}/data.sql
|
||||||
|
|
||||||
# Web
|
# ===============================
|
||||||
spring.thymeleaf.mode=HTML
|
# H2 DATABASE
|
||||||
|
# ===============================
|
||||||
|
spring.datasource.url=jdbc:h2:mem:testdb
|
||||||
|
spring.datasource.driver-class-name=org.h2.Driver
|
||||||
|
spring.datasource.username=sa
|
||||||
|
spring.datasource.password=
|
||||||
|
|
||||||
# JPA
|
spring.h2.console.enabled=true
|
||||||
|
spring.h2.console.path=/h2-console
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# JPA / HIBERNATE (IMPORTANT)
|
||||||
|
# ===============================
|
||||||
spring.jpa.hibernate.ddl-auto=none
|
spring.jpa.hibernate.ddl-auto=none
|
||||||
spring.jpa.open-in-view=false
|
spring.jpa.open-in-view=false
|
||||||
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategySnakeCaseImpl
|
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||||
|
|
||||||
# Internationalization
|
# ✅ MUST BE SINGLE LINE
|
||||||
|
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# THYMELEAF
|
||||||
|
# ===============================
|
||||||
|
spring.thymeleaf.mode=HTML
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# I18N
|
||||||
|
# ===============================
|
||||||
spring.messages.basename=messages/messages
|
spring.messages.basename=messages/messages
|
||||||
|
|
||||||
# Actuator
|
# ===============================
|
||||||
|
# ACTUATOR
|
||||||
|
# ===============================
|
||||||
management.endpoints.web.exposure.include=*
|
management.endpoints.web.exposure.include=*
|
||||||
|
|
||||||
# Logging
|
# ===============================
|
||||||
|
# LOGGING
|
||||||
|
# ===============================
|
||||||
logging.level.org.springframework=INFO
|
logging.level.org.springframework=INFO
|
||||||
# logging.level.org.springframework.web=DEBUG
|
|
||||||
# logging.level.org.springframework.context.annotation=TRACE
|
|
||||||
|
|
||||||
# Maximum time static resources should be cached
|
# ===============================
|
||||||
|
# STATIC CACHE
|
||||||
|
# ===============================
|
||||||
spring.web.resources.cache.cachecontrol.max-age=12h
|
spring.web.resources.cache.cachecontrol.max-age=12h
|
||||||
|
|
|
||||||
|
|
@ -51,3 +51,10 @@ INSERT INTO visits VALUES (default, 7, '2013-01-01', 'rabies shot');
|
||||||
INSERT INTO visits VALUES (default, 8, '2013-01-02', 'rabies shot');
|
INSERT INTO visits VALUES (default, 8, '2013-01-02', 'rabies shot');
|
||||||
INSERT INTO visits VALUES (default, 8, '2013-01-03', 'neutered');
|
INSERT INTO visits VALUES (default, 8, '2013-01-03', 'neutered');
|
||||||
INSERT INTO visits VALUES (default, 7, '2013-01-04', 'spayed');
|
INSERT INTO visits VALUES (default, 7, '2013-01-04', 'spayed');
|
||||||
|
|
||||||
|
INSERT INTO feature_flags (name, enabled, strategy, percentage, user_list, description)
|
||||||
|
VALUES
|
||||||
|
('ADD_PET', true, 'BOOLEAN', 100, NULL, 'Enable add pet feature'),
|
||||||
|
('ADD_VISIT', true, 'BOOLEAN', 100, NULL, 'Enable add visit feature'),
|
||||||
|
('OWNER_SEARCH', true, 'BOOLEAN', 100, NULL, 'Enable owner search');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,3 +62,12 @@ CREATE TABLE visits (
|
||||||
);
|
);
|
||||||
ALTER TABLE visits ADD CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id);
|
ALTER TABLE visits ADD CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id);
|
||||||
CREATE INDEX visits_pet_id ON visits (pet_id);
|
CREATE INDEX visits_pet_id ON visits (pet_id);
|
||||||
|
CREATE TABLE feature_flags (
|
||||||
|
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
enabled BOOLEAN NOT NULL,
|
||||||
|
strategy VARCHAR(50) NOT NULL,
|
||||||
|
percentage INTEGER,
|
||||||
|
user_list VARCHAR(2000),
|
||||||
|
description VARCHAR(255)
|
||||||
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue