Skip to content

Commit 140db4a

Browse files
author
jianggang
authored
feat: add segment history (#37)
add segment history
1 parent bb0a977 commit 140db4a

File tree

14 files changed

+325
-35
lines changed

14 files changed

+325
-35
lines changed

feature-probe-admin/src/main/java/com/featureprobe/api/controller/SegmentController.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
import com.featureprobe.api.base.doc.GetApiResponse;
66
import com.featureprobe.api.base.doc.PatchApiResponse;
77
import com.featureprobe.api.dto.SegmentCreateRequest;
8+
import com.featureprobe.api.dto.SegmentPublishRequest;
89
import com.featureprobe.api.dto.SegmentResponse;
910
import com.featureprobe.api.dto.SegmentSearchRequest;
1011
import com.featureprobe.api.dto.SegmentUpdateRequest;
12+
import com.featureprobe.api.dto.SegmentVersionRequest;
13+
import com.featureprobe.api.dto.SegmentVersionResponse;
1114
import com.featureprobe.api.dto.ToggleSegmentResponse;
1215
import com.featureprobe.api.base.enums.ResponseCodeEnum;
1316
import com.featureprobe.api.base.enums.ValidateTypeEnum;
@@ -66,6 +69,15 @@ public SegmentResponse update(@PathVariable(name = "projectKey") String projectK
6669
return segmentService.update(projectKey, segmentKey, segmentUpdateRequest);
6770
}
6871

72+
@PatchApiResponse
73+
@PatchMapping("/{segmentKey}/publish")
74+
@Operation(summary = "publish segment", description = "publish a segment.")
75+
public SegmentResponse publish(@PathVariable(name = "projectKey") String projectKey,
76+
@PathVariable(name = "segmentKey") String segmentKey,
77+
@RequestBody @Validated SegmentPublishRequest publishRequest) {
78+
return segmentService.publish(projectKey, segmentKey, publishRequest);
79+
}
80+
6981
@DefaultApiResponses
7082
@DeleteMapping("/{segmentKey}")
7183
@Operation(summary = "Delete segment", description = "Delete a segment.")
@@ -84,6 +96,16 @@ public Page<ToggleSegmentResponse> usingToggles(@PathVariable(name = "projectKey
8496
return segmentService.usingToggles(projectKey, segmentKey, paginationRequest);
8597
}
8698

99+
100+
@GetMapping("/{segmentKey}/versions")
101+
@GetApiResponse
102+
@Operation(summary = "List of version by segment", description = "List of version by segment")
103+
public Page<SegmentVersionResponse> versions(@PathVariable(name = "projectKey") String projectKey,
104+
@PathVariable(name = "segmentKey") String segmentKey,
105+
SegmentVersionRequest versionRequest) {
106+
return segmentService.versions(projectKey, segmentKey, versionRequest);
107+
}
108+
87109
@GetApiResponse
88110
@GetMapping("/{segmentKey}")
89111
@Operation(summary = "Get segment", description = "Get a single segment by key.")
@@ -101,4 +123,6 @@ public BaseResponse exists(@PathVariable("projectKey") String projectKey,
101123
segmentService.validateExists(projectKey, type, value);
102124
return new BaseResponse(ResponseCodeEnum.SUCCESS);
103125
}
126+
127+
104128
}

feature-probe-admin/src/main/java/com/featureprobe/api/dto/SegmentCreateRequest.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package com.featureprobe.api.dto;
22

3-
import com.featureprobe.api.base.model.SegmentRuleModel;
43
import lombok.Data;
5-
64
import javax.validation.constraints.NotBlank;
7-
import java.util.List;
85

96
@Data
107
public class SegmentCreateRequest {
@@ -17,5 +14,4 @@ public class SegmentCreateRequest {
1714

1815
private String description;
1916

20-
private List<SegmentRuleModel> rules;
2117
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.featureprobe.api.dto;
2+
3+
import com.featureprobe.api.base.model.SegmentRuleModel;
4+
import lombok.Data;
5+
6+
import java.util.List;
7+
8+
@Data
9+
public class SegmentPublishRequest {
10+
11+
private List<SegmentRuleModel> rules;
12+
13+
private String comment;
14+
15+
}
Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package com.featureprobe.api.dto;
22

3-
import com.featureprobe.api.base.model.SegmentRuleModel;
43
import lombok.Data;
5-
64
import javax.validation.constraints.NotBlank;
7-
import java.util.List;
85

96
@Data
107
public class SegmentUpdateRequest {
@@ -14,6 +11,4 @@ public class SegmentUpdateRequest {
1411

1512
private String description;
1613

17-
private List<SegmentRuleModel> rules;
18-
1914
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.featureprobe.api.dto;
2+
3+
import com.featureprobe.api.base.model.PaginationRequest;
4+
import lombok.Data;
5+
6+
public class SegmentVersionRequest extends PaginationRequest {
7+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.featureprobe.api.dto;
2+
3+
import com.featureprobe.api.base.model.SegmentRuleModel;
4+
import lombok.Data;
5+
import java.util.Date;
6+
import java.util.List;
7+
8+
@Data
9+
public class SegmentVersionResponse {
10+
11+
private String projectKey;
12+
13+
private String key;
14+
15+
private String comment;
16+
17+
private List<SegmentRuleModel> rules;
18+
19+
private Long version;
20+
21+
private Date createdTime;
22+
23+
private String createdBy;
24+
25+
26+
}

feature-probe-admin/src/main/java/com/featureprobe/api/mapper/SegmentMapper.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.featureprobe.api.mapper;
22

33
import com.featureprobe.api.dto.SegmentCreateRequest;
4+
import com.featureprobe.api.dto.SegmentPublishRequest;
45
import com.featureprobe.api.dto.SegmentResponse;
56
import com.featureprobe.api.dto.SegmentUpdateRequest;
67
import com.featureprobe.api.dto.ToggleSegmentResponse;
@@ -25,7 +26,6 @@ public interface SegmentMapper extends BaseMapper {
2526

2627
SegmentMapper INSTANCE = Mappers.getMapper(SegmentMapper.class);
2728

28-
@Mapping(target = "rules", expression = "java(toSegmentRulesString(createRequest.getRules()))")
2929
Segment requestToEntity(SegmentCreateRequest createRequest);
3030

3131
@Mapping(target = "rules", expression = "java(toSegmentRules(segment.getRules()))")
@@ -34,10 +34,13 @@ public interface SegmentMapper extends BaseMapper {
3434

3535
ToggleSegmentResponse toggleToToggleSegment(Toggle toggle);
3636

37-
@Mapping(target = "rules", expression = "java(toSegmentRulesString(updateRequest.getRules()))")
3837
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
3938
void mapEntity(SegmentUpdateRequest updateRequest, @MappingTarget Segment segment);
4039

40+
@Mapping(target = "rules", expression = "java(toSegmentRulesString(publishRequest.getRules()))")
41+
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
42+
void mapEntity(SegmentPublishRequest publishRequest, @MappingTarget Segment segment);
43+
4144
default String toSegmentRulesString(List<SegmentRuleModel> rules) {
4245
if (!CollectionUtils.isEmpty(rules)) {
4346
return JsonMapper.toJSONString(rules);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.featureprobe.api.mapper;
2+
3+
import com.featureprobe.api.base.model.SegmentRuleModel;
4+
import com.featureprobe.api.base.util.JsonMapper;
5+
import com.featureprobe.api.dao.entity.SegmentVersion;
6+
import com.featureprobe.api.dto.SegmentVersionResponse;
7+
import org.apache.commons.lang3.StringUtils;
8+
import org.mapstruct.Mapper;
9+
import org.mapstruct.Mapping;
10+
import org.mapstruct.factory.Mappers;
11+
12+
import java.util.Collections;
13+
import java.util.List;
14+
15+
@Mapper
16+
public interface SegmentVersionMapper extends BaseMapper {
17+
18+
SegmentVersionMapper INSTANCE = Mappers.getMapper(SegmentVersionMapper.class);
19+
20+
@Mapping(target = "rules", expression = "java(toSegmentRules(segmentVersion.getRules()))")
21+
SegmentVersionResponse entityToResponse(SegmentVersion segmentVersion);
22+
23+
default List<SegmentRuleModel> toSegmentRules(String rules) {
24+
if (StringUtils.isNotBlank(rules)) {
25+
return JsonMapper.toObject(rules, List.class);
26+
}
27+
return toDefaultSegmentRules();
28+
}
29+
30+
default List<SegmentRuleModel> toDefaultSegmentRules() {
31+
return Collections.emptyList();
32+
}
33+
34+
}

feature-probe-admin/src/main/java/com/featureprobe/api/service/SegmentService.java

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
package com.featureprobe.api.service;
22

33
import com.featureprobe.api.base.constants.MessageKey;
4+
import com.featureprobe.api.base.util.JsonMapper;
5+
import com.featureprobe.api.dao.entity.SegmentVersion;
46
import com.featureprobe.api.dao.exception.ResourceConflictException;
57
import com.featureprobe.api.dao.exception.ResourceNotFoundException;
8+
import com.featureprobe.api.dao.repository.SegmentVersionRepository;
69
import com.featureprobe.api.dao.utils.PageRequestUtil;
710
import com.featureprobe.api.dto.SegmentCreateRequest;
11+
import com.featureprobe.api.dto.SegmentPublishRequest;
812
import com.featureprobe.api.dto.SegmentResponse;
913
import com.featureprobe.api.dto.SegmentSearchRequest;
1014
import com.featureprobe.api.dto.SegmentUpdateRequest;
15+
import com.featureprobe.api.dto.SegmentVersionRequest;
16+
import com.featureprobe.api.dto.SegmentVersionResponse;
1117
import com.featureprobe.api.dto.ToggleSegmentResponse;
1218
import com.featureprobe.api.dao.entity.Environment;
1319
import com.featureprobe.api.dao.entity.Project;
@@ -26,6 +32,7 @@
2632
import com.featureprobe.api.dao.repository.TargetingSegmentRepository;
2733
import com.featureprobe.api.dao.repository.ToggleRepository;
2834
import com.featureprobe.api.base.model.PaginationRequest;
35+
import com.featureprobe.api.mapper.SegmentVersionMapper;
2936
import lombok.AllArgsConstructor;
3037
import lombok.extern.slf4j.Slf4j;
3138
import org.apache.commons.collections4.CollectionUtils;
@@ -36,10 +43,12 @@
3643
import org.springframework.data.domain.Sort;
3744
import org.springframework.data.jpa.domain.Specification;
3845
import org.springframework.stereotype.Service;
46+
import org.springframework.transaction.annotation.Transactional;
3947

4048
import javax.persistence.EntityManager;
4149
import javax.persistence.PersistenceContext;
4250
import javax.persistence.criteria.Predicate;
51+
import java.util.Collections;
4352
import java.util.List;
4453
import java.util.Optional;
4554
import java.util.Set;
@@ -61,6 +70,9 @@ public class SegmentService {
6170
private EnvironmentRepository environmentRepository;
6271

6372
private ProjectRepository projectRepository;
73+
74+
private SegmentVersionRepository segmentVersionRepository;
75+
6476
private ChangeLogService changeLogService;
6577

6678
@PersistenceContext
@@ -79,30 +91,68 @@ public SegmentResponse create(String projectKey, SegmentCreateRequest createRequ
7991
Segment segment = SegmentMapper.INSTANCE.requestToEntity(createRequest);
8092
segment.setProjectKey(projectKey);
8193
segment.setUniqueKey(StringUtils.join(projectKey, "$", createRequest.getKey()));
82-
if (CollectionUtils.isNotEmpty(project.getEnvironments())) {
83-
for (Environment environment : project.getEnvironments()) {
84-
changeLogService.create(environment, ChangeLogType.CHANGE);
85-
}
86-
}
87-
return SegmentMapper.INSTANCE.entityToResponse(segmentRepository.save(segment));
94+
segment.setRules(JsonMapper.toJSONString(Collections.emptyList()));
95+
segment.setVersion(1L);
96+
saveSegmentChangeLog(project);
97+
Segment savedSegment = segmentRepository.save(segment);
98+
saveSegmentVersion(buildSegmentVersion(savedSegment, null, null));
99+
return SegmentMapper.INSTANCE.entityToResponse(savedSegment);
88100
}
89101

102+
@Transactional(rollbackFor = Exception.class)
90103
public SegmentResponse update(String projectKey, String segmentKey, SegmentUpdateRequest updateRequest) {
91-
Segment segment = segmentRepository.findByProjectKeyAndKey(projectKey, segmentKey);
104+
Project project = projectRepository.findByKey(projectKey).orElseThrow(() ->
105+
new ResourceNotFoundException(ResourceType.PROJECT, projectKey));
106+
Segment segment = segmentRepository.findByProjectKeyAndKey(projectKey, segmentKey).orElseThrow(() ->
107+
new ResourceNotFoundException(ResourceType.SEGMENT, projectKey + "_" + segmentKey));
92108
if (!StringUtils.equals(segment.getName(), updateRequest.getName())) {
93109
validateName(projectKey, updateRequest.getName());
94110
}
95111
SegmentMapper.INSTANCE.mapEntity(updateRequest, segment);
112+
saveSegmentChangeLog(project);
96113
return SegmentMapper.INSTANCE.entityToResponse(segmentRepository.save(segment));
97114
}
98115

116+
@Transactional(rollbackFor = Exception.class)
117+
public SegmentResponse publish(String projectKey, String segmentKey, SegmentPublishRequest publishRequest) {
118+
Project project = projectRepository.findByKey(projectKey).orElseThrow(() ->
119+
new ResourceNotFoundException(ResourceType.PROJECT, projectKey));
120+
Segment segment = segmentRepository.findByProjectKeyAndKey(projectKey, segmentKey).orElseThrow(() ->
121+
new ResourceNotFoundException(ResourceType.SEGMENT, projectKey + "_" + segmentKey));
122+
Long oldVersion = segment.getVersion();
123+
SegmentMapper.INSTANCE.mapEntity(publishRequest, segment);
124+
Segment updatedSegment = segmentRepository.saveAndFlush(segment);
125+
if (updatedSegment.getVersion() > oldVersion) {
126+
saveSegmentVersion(buildSegmentVersion(updatedSegment, publishRequest.getComment(), null));
127+
}
128+
saveSegmentChangeLog(project);
129+
return SegmentMapper.INSTANCE.entityToResponse(updatedSegment);
130+
}
131+
132+
private void saveSegmentChangeLog(Project project) {
133+
if (CollectionUtils.isNotEmpty(project.getEnvironments())) {
134+
for (Environment environment : project.getEnvironments()) {
135+
changeLogService.create(environment, ChangeLogType.CHANGE);
136+
}
137+
}
138+
}
139+
140+
public Page<SegmentVersionResponse> versions(String projectKey, String segmentKey,
141+
SegmentVersionRequest versionRequest) {
142+
Specification<SegmentVersion> spec = buildVersionsQuerySpec(projectKey, segmentKey);
143+
Page<SegmentVersion> versions = segmentVersionRepository.findAll(spec,
144+
PageRequestUtil.toPageable(versionRequest, Sort.Direction.DESC, "version"));
145+
return versions.map(version -> SegmentVersionMapper.INSTANCE.entityToResponse(version));
146+
}
147+
99148
public SegmentResponse delete(String projectKey, String segmentKey) {
100149
Project project = projectRepository.findByKey(projectKey).orElseThrow(() ->
101150
new ResourceNotFoundException(ResourceType.PROJECT, projectKey));
102151
if (targetingSegmentRepository.countByProjectKeyAndSegmentKey(projectKey, segmentKey) > 0) {
103152
throw new IllegalArgumentException(MessageKey.USING);
104153
}
105-
Segment segment = segmentRepository.findByProjectKeyAndKey(projectKey, segmentKey);
154+
Segment segment = segmentRepository.findByProjectKeyAndKey(projectKey, segmentKey).orElseThrow(() ->
155+
new ResourceNotFoundException(ResourceType.SEGMENT, projectKey + "_" + segmentKey));
106156
segment.setDeleted(true);
107157
if (CollectionUtils.isNotEmpty(project.getEnvironments())) {
108158
for (Environment environment : project.getEnvironments()) {
@@ -142,10 +192,25 @@ public Page<ToggleSegmentResponse> usingToggles(String projectKey, String segmen
142192
}
143193

144194
public SegmentResponse queryByKey(String projectKey, String segmentKey) {
145-
Segment segment = segmentRepository.findByProjectKeyAndKey(projectKey, segmentKey);
195+
Segment segment = segmentRepository.findByProjectKeyAndKey(projectKey, segmentKey).orElseThrow(() ->
196+
new ResourceNotFoundException(ResourceType.SEGMENT, projectKey + "_" + segmentKey));
146197
return SegmentMapper.INSTANCE.entityToResponse(segment);
147198
}
148199

200+
private SegmentVersion buildSegmentVersion(Segment segment, String comment, Long approvalId) {
201+
SegmentVersion segmentVersion = new SegmentVersion();
202+
segmentVersion.setVersion(segment.getVersion());
203+
segmentVersion.setKey(segment.getKey());
204+
segmentVersion.setRules(segment.getRules());
205+
segmentVersion.setComment(comment);
206+
segmentVersion.setApprovalId(approvalId);
207+
segmentVersion.setProjectKey(segment.getProjectKey());
208+
return segmentVersion;
209+
}
210+
private void saveSegmentVersion(SegmentVersion segmentVersion) {
211+
segmentVersionRepository.save(segmentVersion);
212+
}
213+
149214
private Specification<Segment> buildQuerySpec(String projectKey, String keyword) {
150215
return (root, query, cb) -> {
151216
Predicate p3 = cb.equal(root.get("projectKey"), projectKey);
@@ -166,6 +231,15 @@ private Page<SegmentResponse> findPagingBySpec(Specification<Segment> spec, Page
166231
return segments.map(segment -> SegmentMapper.INSTANCE.entityToResponse(segment));
167232
}
168233

234+
235+
private Specification<SegmentVersion> buildVersionsQuerySpec(String projectKey, String key) {
236+
return (root, query, cb) -> {
237+
Predicate p1 = cb.equal(root.get("projectKey"), projectKey);
238+
Predicate p2 = cb.equal(root.get("key"), key);
239+
return query.where(cb.and(p1, p2)).getRestriction();
240+
};
241+
}
242+
169243
public void validateExists(String projectKey, ValidateTypeEnum type, String value) {
170244
switch (type) {
171245
case KEY:
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
create table segment_version
2+
(
3+
id int auto_increment primary key,
4+
organization_id bigint default 1 not null,
5+
project_key varchar(128) default '' not null,
6+
`key` varchar(128) default '' not null,
7+
comment varchar(1024) default "" not null,
8+
rules text not null,
9+
version bigint default 0 not null,
10+
modified_time datetime default CURRENT_TIMESTAMP not null,
11+
created_by bigint not null,
12+
created_time datetime default CURRENT_TIMESTAMP not null,
13+
modified_by bigint not null,
14+
approval_id bigint null
15+
) collate = utf8mb4_unicode_ci;
16+
17+
UPDATE segment SET version = 1;
18+
Insert into segment_version(organization_id, project_key, `key`, rules, version, modified_time, created_by, created_time, modified_by) Select organization_id, project_key, `key`, rules, 1, modified_time, created_by, created_time, modified_by from segment ;

0 commit comments

Comments
 (0)