mirror of
https://github.com/spring-projects/spring-petclinic.git
synced 2026-02-11 17:01:11 +00:00
Added feature flags logic (whitelist, blacklist and rollout percentage included)
This commit is contained in:
parent
ab1d5364a0
commit
1435d0d694
13 changed files with 521 additions and 0 deletions
71
README.md
71
README.md
|
|
@ -172,3 +172,74 @@ For additional details, please refer to the blog post [Hello DCO, Goodbye CLA: S
|
|||
## License
|
||||
|
||||
The Spring PetClinic sample application is released under version 2.0 of the [Apache License](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
|
||||
# PetClinic Feature Flags
|
||||
- This project adds feature flags to the PetClinic app so that we can control the features to certain users.
|
||||
|
||||
# How it works:
|
||||
- Enable/Disable:
|
||||
Each feature can be enabled/disabled for the users based on if they are whitelisted or blacklisted. There is also a global disability which shuts down the specified feature until enabled.
|
||||
- Whitelist:
|
||||
Only users listed in the whitelist can access the feature specified.
|
||||
- Blacklist:
|
||||
Users listed in the blacklist will be blocked from the feature even if the feature is enabled.
|
||||
- Rollout Percentage:
|
||||
You can release a feature gradually instead of a global release. For example, if rollout_percentage = 50, roughly 50% of users get access (deterministic by user ID).
|
||||
|
||||
# Example:
|
||||
| Feature Key | Enabled | Blacklist | Whitelist | Rollout % |
|
||||
| ------------ | ------- | --------- | --------- | --------- |
|
||||
| ADD_PET | true | 4 | 1 | 100 |
|
||||
| OWNER_SEARCH | true | - | - | 50 |
|
||||
|
||||
Owner with id 4 is blacklisted i.e. they cannot add pets.
|
||||
Owner with id 1 is whiteliste i.e. they can addpets.
|
||||
The rollout percentage of OWNER_SEARCH key is 50% i.e. roughly half the users can search for owners.
|
||||
|
||||
# Using Feature Flags in Code:
|
||||
@FeatureFlagEnabled("ADD_PET")
|
||||
@GetMapping("/owners/{ownerId}/pets/new")
|
||||
public String newPetForm(@PathVariable int ownerId) {
|
||||
return "pets/createPetForm";
|
||||
}
|
||||
|
||||
The Aspect intercepts the method and checks if the feature i.e. ADD_PET is:
|
||||
1. Enabled
|
||||
2. If user is whitelisted/blacklisted
|
||||
3. Rollout percentage
|
||||
If access is denied, the user gets HTTP 403 Forbidden.
|
||||
|
||||
# Database Setup:
|
||||
- Tables
|
||||
CREATE TABLE feature_flags (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
flag_key VARCHAR(100) UNIQUE,
|
||||
is_enabled BOOLEAN,
|
||||
globally_disabled BOOLEAN,
|
||||
description VARCHAR(255),
|
||||
rollout_percentage INT
|
||||
);
|
||||
|
||||
CREATE TABLE feature_flag_blacklist (
|
||||
feature_flag_id INT,
|
||||
owner_id INT
|
||||
);
|
||||
|
||||
CREATE TABLE feature_flag_whitelist (
|
||||
feature_flag_id INT,
|
||||
owner_id INT
|
||||
);
|
||||
|
||||
- Example Data
|
||||
INSERT INTO feature_flags (flag_key, is_enabled, globally_disabled, description, rollout_percentage)
|
||||
VALUES ('ADD_PET', true, false, 'Allows adding pets', 100);
|
||||
|
||||
INSERT INTO feature_flag_blacklist (feature_flag_id, owner_id) VALUES (1, 4);
|
||||
INSERT INTO feature_flag_whitelist (feature_flag_id, owner_id) VALUES (1, 1);
|
||||
|
||||
# Why Feature Flags?
|
||||
- Roll out features gradually.
|
||||
- Access control i.e. privilege to users.
|
||||
- Feature blocking whenever needed i.e. internal errors or data corruption.
|
||||
- Avoid frequent code redeployment for feature changes.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.persistence.CollectionTable;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.ElementCollection;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
@Entity
|
||||
@Table(name = "feature_flags")
|
||||
public class FeatureFlag extends BaseEntity {
|
||||
|
||||
@Column(name = "flag_key", unique = true)
|
||||
@NotBlank
|
||||
private String key;
|
||||
|
||||
@Column(name = "is_enabled")
|
||||
private boolean isEnabled;
|
||||
|
||||
@Column(name = "globally_disabled")
|
||||
private boolean globallyDisabled;
|
||||
|
||||
@Column(name = "rollout_percentage")
|
||||
private Integer rolloutPercentage;
|
||||
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "feature_flag_blacklist", joinColumns = @JoinColumn(name = "feature_flag_id"))
|
||||
@JsonIgnore
|
||||
@Column(name = "owner_id")
|
||||
private List<Integer> blacklistedUsers = new ArrayList<>();
|
||||
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "feature_flag_whitelist", joinColumns = @JoinColumn(name = "feature_flag_id"))
|
||||
@JsonIgnore
|
||||
@Column(name = "owner_id")
|
||||
private List<Integer> whitelistedUsers = new ArrayList<>();
|
||||
|
||||
public boolean isAllowedForOwner(Integer ownerId) {
|
||||
if (globallyDisabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ownerId != null && blacklistedUsers.contains(ownerId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ownerId != null && whitelistedUsers.contains(ownerId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rolloutPercentage != null && ownerId != null) {
|
||||
int bucket = Math.abs(ownerId.hashCode() % 100);
|
||||
return bucket < rolloutPercentage;
|
||||
}
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.isEnabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isGloballyDisabled() {
|
||||
return globallyDisabled;
|
||||
}
|
||||
|
||||
public void setGloballyDisabled(boolean globallyDisabled) {
|
||||
this.globallyDisabled = globallyDisabled;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public List<Integer> getBlacklistedUsers() {
|
||||
return blacklistedUsers;
|
||||
}
|
||||
|
||||
public void setBlacklistedUsers(List<Integer> blacklistedUsers) {
|
||||
this.blacklistedUsers = blacklistedUsers;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public List<Integer> getWhitelistedUsers() {
|
||||
return whitelistedUsers;
|
||||
}
|
||||
|
||||
public void setWhitelistedUsers(List<Integer> whitelistedUsers) {
|
||||
this.whitelistedUsers = whitelistedUsers;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import java.util.Objects;
|
|||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.samples.petclinic.system.FeatureFlagEnabled;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
|
@ -69,11 +70,13 @@ class OwnerController {
|
|||
+ ". Please ensure the ID is correct " + "and the owner exists in the database."));
|
||||
}
|
||||
|
||||
@FeatureFlagEnabled("OWNER_ADD")
|
||||
@GetMapping("/owners/new")
|
||||
public String initCreationForm() {
|
||||
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
|
||||
}
|
||||
|
||||
@FeatureFlagEnabled("OWNER_ADD")
|
||||
@PostMapping("/owners/new")
|
||||
public String processCreationForm(@Valid Owner owner, BindingResult result, RedirectAttributes redirectAttributes) {
|
||||
if (result.hasErrors()) {
|
||||
|
|
@ -92,6 +95,7 @@ class OwnerController {
|
|||
}
|
||||
|
||||
@GetMapping("/owners")
|
||||
@FeatureFlagEnabled("OWNER_SEARCH")
|
||||
public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner owner, BindingResult result,
|
||||
Model model) {
|
||||
// allow parameterless GET request for /owners to return all records
|
||||
|
|
@ -133,11 +137,13 @@ class OwnerController {
|
|||
return owners.findByLastNameStartingWith(lastname, pageable);
|
||||
}
|
||||
|
||||
@FeatureFlagEnabled("OWNER_EDIT")
|
||||
@GetMapping("/owners/{ownerId}/edit")
|
||||
public String initUpdateOwnerForm() {
|
||||
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
|
||||
}
|
||||
|
||||
@FeatureFlagEnabled("OWNER_EDIT")
|
||||
@PostMapping("/owners/{ownerId}/edit")
|
||||
public String processUpdateOwnerForm(@Valid Owner owner, BindingResult result, @PathVariable("ownerId") int ownerId,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import java.util.Objects;
|
|||
import java.util.Optional;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.samples.petclinic.system.FeatureFlagEnabled;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
|
@ -95,6 +96,7 @@ class PetController {
|
|||
dataBinder.setValidator(new PetValidator());
|
||||
}
|
||||
|
||||
@FeatureFlagEnabled("ADD_PET")
|
||||
@GetMapping("/pets/new")
|
||||
public String initCreationForm(Owner owner, ModelMap model) {
|
||||
Pet pet = new Pet();
|
||||
|
|
@ -103,6 +105,7 @@ class PetController {
|
|||
}
|
||||
|
||||
@PostMapping("/pets/new")
|
||||
@FeatureFlagEnabled("ADD_PET")
|
||||
public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
|
||||
|
|
@ -124,11 +127,13 @@ class PetController {
|
|||
return "redirect:/owners/{ownerId}";
|
||||
}
|
||||
|
||||
@FeatureFlagEnabled("EDIT_PET")
|
||||
@GetMapping("/pets/{petId}/edit")
|
||||
public String initUpdateForm() {
|
||||
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
|
||||
}
|
||||
|
||||
@FeatureFlagEnabled("EDIT_PET")
|
||||
@PostMapping("/pets/{petId}/edit")
|
||||
public String processUpdateForm(Owner owner, @Valid Pet pet, BindingResult result,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import java.util.Map;
|
|||
import java.util.Optional;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.samples.petclinic.system.FeatureFlagEnabled;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
|
@ -89,6 +90,7 @@ class VisitController {
|
|||
// Spring MVC calls method loadPetWithVisit(...) before processNewVisitForm is
|
||||
// called
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/visits/new")
|
||||
@FeatureFlagEnabled("ADD_VISIT")
|
||||
public String processNewVisitForm(@ModelAttribute Owner owner, @PathVariable int petId, @Valid Visit visit,
|
||||
BindingResult result, RedirectAttributes redirectAttributes) {
|
||||
if (result.hasErrors()) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
public class FeatureFlagAspect {
|
||||
|
||||
private final FeatureFlagHelper featureFlagHelper;
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
public FeatureFlagAspect(FeatureFlagHelper featureFlagHelper, HttpServletRequest request) {
|
||||
this.featureFlagHelper = featureFlagHelper;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Around("@annotation(featureFlagEnabled)")
|
||||
public Object checkFeatureFlag(ProceedingJoinPoint joinPoint, FeatureFlagEnabled featureFlagEnabled)
|
||||
throws Throwable {
|
||||
String featureFlagKey = featureFlagEnabled.value();
|
||||
Integer ownerId = extractOwnerIdFromPath();
|
||||
boolean enabled = featureFlagHelper.isEnabled(featureFlagKey, ownerId);
|
||||
if (!enabled) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Feature '" + featureFlagKey + "' is disabled");
|
||||
}
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
private Integer extractOwnerIdFromPath() {
|
||||
String uri = request.getRequestURI();
|
||||
String[] parts = uri.split("/");
|
||||
for (int i = 0; i < parts.length - 1; i++) {
|
||||
if ("owners".equals(parts[i])) {
|
||||
try {
|
||||
return Integer.parseInt(parts[i + 1]);
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 java.util.List;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.samples.petclinic.model.FeatureFlag;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/feature-flags")
|
||||
public class FeatureFlagController {
|
||||
|
||||
private final FeatureFlagRepository featureFlagRepository;
|
||||
|
||||
public FeatureFlagController(FeatureFlagRepository featureFlagRepository) {
|
||||
this.featureFlagRepository = featureFlagRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<FeatureFlag>> getAllFlags() {
|
||||
List<FeatureFlag> flags = (List<FeatureFlag>) featureFlagRepository.findAll();
|
||||
return ResponseEntity.ok(flags);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<FeatureFlag> createFlag(@RequestBody FeatureFlag featureFlag) {
|
||||
FeatureFlag saved = featureFlagRepository.save(featureFlag);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<FeatureFlag> updateFlag(@PathVariable Integer id, @RequestBody FeatureFlag featureFlag) {
|
||||
if (!featureFlagRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
featureFlag.setId(id);
|
||||
FeatureFlag updated = featureFlagRepository.save(featureFlag);
|
||||
return ResponseEntity.ok(updated);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteFlag(@PathVariable Integer id) {
|
||||
if (!featureFlagRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
featureFlagRepository.findById(id).ifPresent(flag -> {
|
||||
flag.getBlacklistedUsers().clear();
|
||||
flag.getWhitelistedUsers().clear();
|
||||
featureFlagRepository.delete(flag);
|
||||
});
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation to restrict access to a method based on a feature flag.
|
||||
*
|
||||
* If the feature flag is disabled or globally disabled, the method will not be executed.
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface FeatureFlagEnabled {
|
||||
|
||||
/**
|
||||
* The key of the feature flag to check.
|
||||
*/
|
||||
String value();
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.samples.petclinic.model.FeatureFlag;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class FeatureFlagHelper {
|
||||
|
||||
private final FeatureFlagRepository featureFlagRepository;
|
||||
|
||||
public FeatureFlagHelper(FeatureFlagRepository featureFlagRepository) {
|
||||
this.featureFlagRepository = featureFlagRepository;
|
||||
}
|
||||
|
||||
public boolean isEnabled(String flagKey, Integer userId) {
|
||||
FeatureFlag flag = featureFlagRepository.findByKey(flagKey);
|
||||
|
||||
if (flag == null) {
|
||||
return false; // fail-safe default
|
||||
}
|
||||
|
||||
return flag.isAllowedForOwner(userId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.samples.petclinic.model.FeatureFlag;
|
||||
|
||||
public interface FeatureFlagRepository extends JpaRepository<FeatureFlag, Integer> {
|
||||
|
||||
FeatureFlag findByKey(String key);
|
||||
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ package org.springframework.samples.petclinic.vet;
|
|||
import java.util.List;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.samples.petclinic.system.FeatureFlagEnabled;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
|
@ -41,6 +42,7 @@ class VetController {
|
|||
this.vetRepository = vetRepository;
|
||||
}
|
||||
|
||||
@FeatureFlagEnabled("VIEW_VETS")
|
||||
@GetMapping("/vets.html")
|
||||
public String showVetList(@RequestParam(defaultValue = "1") int page, Model model) {
|
||||
// Here we are returning an object of type 'Vets' rather than a collection of Vet
|
||||
|
|
@ -66,6 +68,7 @@ class VetController {
|
|||
return vetRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
@FeatureFlagEnabled("VIEW_VETS")
|
||||
@GetMapping({ "/vets" })
|
||||
public @ResponseBody Vets showResourcesVetList() {
|
||||
// Here we are returning an object of type 'Vets' rather than a collection of Vet
|
||||
|
|
|
|||
|
|
@ -51,3 +51,18 @@ 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-03', 'neutered');
|
||||
INSERT INTO visits VALUES (default, 7, '2013-01-04', 'spayed');
|
||||
|
||||
INSERT INTO feature_flags (flag_key, is_enabled, globally_disabled, description) VALUES ('ADD_PET', false, false, 'Allows adding new pets');
|
||||
INSERT INTO feature_flags (flag_key, is_enabled, globally_disabled, description) VALUES ('EDIT_PET', true, false, 'Allows editing existing pets');
|
||||
INSERT INTO feature_flags (flag_key, is_enabled, globally_disabled, description) VALUES ('ADD_VISIT', true, false, 'Allows adding new visits');
|
||||
INSERT INTO feature_flags (flag_key, is_enabled, globally_disabled, description) VALUES ('OWNER_SEARCH', true, false, 'Allows searching for owners');
|
||||
INSERT INTO feature_flags (flag_key, is_enabled, globally_disabled, description) VALUES ('OWNER_EDIT', true, false, 'Allows editing owner information');
|
||||
INSERT INTO feature_flags (flag_key, is_enabled, globally_disabled, description) VALUES ('OWNER_ADD', true, false, 'Allows adding new owners');
|
||||
INSERT INTO feature_flags (flag_key, is_enabled, globally_disabled, description) VALUES ('VIEW_VETS', true, false, 'Allows viewing vet information');
|
||||
|
||||
-- id 5 can't edit owners
|
||||
INSERT INTO feature_flag_blacklist (feature_flag_id, owner_id) VALUES (1, 5);
|
||||
|
||||
-- only id 1 can add pets and owners
|
||||
INSERT INTO feature_flag_whitelist (feature_flag_id, owner_id) VALUES (1, 1);
|
||||
INSERT INTO feature_flag_whitelist (feature_flag_id, owner_id) VALUES (6, 1);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ DROP TABLE visits IF EXISTS;
|
|||
DROP TABLE pets IF EXISTS;
|
||||
DROP TABLE types IF EXISTS;
|
||||
DROP TABLE owners IF EXISTS;
|
||||
DROP TABLE feature_flags IF EXISTS;
|
||||
DROP TABLE feature_flag_blacklist IF EXISTS;
|
||||
|
||||
|
||||
CREATE TABLE vets (
|
||||
|
|
@ -62,3 +64,36 @@ CREATE TABLE visits (
|
|||
);
|
||||
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 TABLE feature_flags (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
flag_key VARCHAR(100) NOT NULL,
|
||||
is_enabled BOOLEAN,
|
||||
globally_disabled BOOLEAN,
|
||||
rollout_percentage INTEGER,
|
||||
description VARCHAR(255)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_feature_flags_key ON feature_flags(flag_key);
|
||||
|
||||
CREATE TABLE feature_flag_blacklist (
|
||||
feature_flag_id INTEGER NOT NULL,
|
||||
owner_id INTEGER NOT NULL,
|
||||
CONSTRAINT fk_feature_blacklist_flag
|
||||
FOREIGN KEY (feature_flag_id)
|
||||
REFERENCES feature_flags(id),
|
||||
CONSTRAINT fk_blacklist_owner
|
||||
FOREIGN KEY (owner_id)
|
||||
REFERENCES owners(id)
|
||||
);
|
||||
|
||||
CREATE TABLE feature_flag_whitelist (
|
||||
feature_flag_id INTEGER NOT NULL,
|
||||
owner_id INTEGER NOT NULL,
|
||||
CONSTRAINT fk_feature_whitelist_flag
|
||||
FOREIGN KEY (feature_flag_id)
|
||||
REFERENCES feature_flags(id),
|
||||
CONSTRAINT fk_whitelist_owner
|
||||
FOREIGN KEY (owner_id)
|
||||
REFERENCES owners(id)
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue