Skip to content

Commit 409d320

Browse files
authored
feat(calm-hub): nest mapping controller under /mappings and add link-… (finos#2633)
* feat(calm-hub): nest mapping controller under /mappings and add link-to-existing endpoint Rename Front Controller feature to Mapping Controller and nest its REST resources under /mappings/ so a customId cannot shadow reserved path segments. Add a PUT endpoint to map a customId to an existing resource, validating that the target exists within the same namespace. Closes finos#2628 * fix(calm-hub): apply @Valid @NotNull to linkExistingResource request body Address Will's review feedback on PR finos#2633: replace the raw String requestBody parameter and manual JSON parsing / null checks in linkExistingResource with a typed, bean-validated MappingLinkRequest DTO, matching the convention used by all other resource endpoints (PatternResource, NamespaceResource, etc.).
1 parent 0d364c6 commit 409d320

11 files changed

Lines changed: 528 additions & 138 deletions

calm-hub/src/integration-test/java/integration/MongoFrontControllerIntegration.java renamed to calm-hub/src/integration-test/java/integration/MongoMappingControllerIntegration.java

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
@QuarkusTest
2121
@TestProfile(IntegrationTestProfile.class)
2222
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
23-
public class MongoFrontControllerIntegration {
23+
public class MongoMappingControllerIntegration {
2424

25-
private static final Logger logger = LoggerFactory.getLogger(MongoFrontControllerIntegration.class);
25+
private static final Logger logger = LoggerFactory.getLogger(MongoMappingControllerIntegration.class);
2626

2727
@BeforeEach
2828
public void setup() {
@@ -63,17 +63,17 @@ void create_pattern_via_front_controller() {
6363
given()
6464
.body(payload)
6565
.header("Content-Type", "application/json")
66-
.when().post("/calm/namespaces/finos/test-pattern")
66+
.when().post("/calm/namespaces/finos/mappings/test-pattern")
6767
.then()
6868
.statusCode(201)
69-
.header("Location", containsString("/calm/namespaces/finos/test-pattern/versions/1.0.0"));
69+
.header("Location", containsString("/calm/namespaces/finos/mappings/test-pattern/versions/1.0.0"));
7070
}
7171

7272
@Test
7373
@Order(2)
7474
void get_latest_version_by_custom_id() {
7575
given()
76-
.when().get("/calm/namespaces/finos/test-pattern")
76+
.when().get("/calm/namespaces/finos/mappings/test-pattern")
7777
.then()
7878
.statusCode(200)
7979
.body(containsString("front-controller-test"));
@@ -83,7 +83,7 @@ void get_latest_version_by_custom_id() {
8383
@Order(3)
8484
void get_specific_version_by_custom_id() {
8585
given()
86-
.when().get("/calm/namespaces/finos/test-pattern/versions/1.0.0")
86+
.when().get("/calm/namespaces/finos/mappings/test-pattern/versions/1.0.0")
8787
.then()
8888
.statusCode(200)
8989
.body(containsString("front-controller-test"));
@@ -93,7 +93,7 @@ void get_specific_version_by_custom_id() {
9393
@Order(4)
9494
void list_versions_by_custom_id() {
9595
given()
96-
.when().get("/calm/namespaces/finos/test-pattern/versions")
96+
.when().get("/calm/namespaces/finos/mappings/test-pattern/versions")
9797
.then()
9898
.statusCode(200)
9999
.body("values", hasSize(1))
@@ -113,17 +113,17 @@ void update_resource_with_minor_version_bump() {
113113
given()
114114
.body(payload)
115115
.header("Content-Type", "application/json")
116-
.when().post("/calm/namespaces/finos/test-pattern")
116+
.when().post("/calm/namespaces/finos/mappings/test-pattern")
117117
.then()
118118
.statusCode(201)
119-
.header("Location", containsString("/calm/namespaces/finos/test-pattern/versions/1.1.0"));
119+
.header("Location", containsString("/calm/namespaces/finos/mappings/test-pattern/versions/1.1.0"));
120120
}
121121

122122
@Test
123123
@Order(6)
124124
void get_latest_returns_newest_version() {
125125
given()
126-
.when().get("/calm/namespaces/finos/test-pattern")
126+
.when().get("/calm/namespaces/finos/mappings/test-pattern")
127127
.then()
128128
.statusCode(200)
129129
.body(containsString("front-controller-test-v2"));
@@ -133,7 +133,7 @@ void get_latest_returns_newest_version() {
133133
@Order(7)
134134
void original_version_still_accessible() {
135135
given()
136-
.when().get("/calm/namespaces/finos/test-pattern/versions/1.0.0")
136+
.when().get("/calm/namespaces/finos/mappings/test-pattern/versions/1.0.0")
137137
.then()
138138
.statusCode(200)
139139
.body(containsString("front-controller-test"));
@@ -143,7 +143,7 @@ void original_version_still_accessible() {
143143
@Order(8)
144144
void list_versions_shows_both() {
145145
given()
146-
.when().get("/calm/namespaces/finos/test-pattern/versions")
146+
.when().get("/calm/namespaces/finos/mappings/test-pattern/versions")
147147
.then()
148148
.statusCode(200)
149149
.body("values", hasSize(2))
@@ -175,17 +175,67 @@ void duplicate_custom_id_same_namespace_returns_update() {
175175
given()
176176
.body(payload)
177177
.header("Content-Type", "application/json")
178-
.when().post("/calm/namespaces/finos/test-pattern")
178+
.when().post("/calm/namespaces/finos/mappings/test-pattern")
179179
.then()
180180
.statusCode(201)
181-
.header("Location", containsString("/calm/namespaces/finos/test-pattern/versions/1.1.1"));
181+
.header("Location", containsString("/calm/namespaces/finos/mappings/test-pattern/versions/1.1.1"));
182182
}
183183

184184
@Test
185185
@Order(11)
186186
void return_404_for_nonexistent_custom_id() {
187187
given()
188-
.when().get("/calm/namespaces/finos/nonexistent-resource")
188+
.when().get("/calm/namespaces/finos/mappings/nonexistent-resource")
189+
.then()
190+
.statusCode(404);
191+
}
192+
193+
@Test
194+
@Order(12)
195+
void link_existing_resource_to_new_custom_id() {
196+
Integer numericId = given()
197+
.when().get("/calm/namespaces/finos/mappings?type=PATTERN")
198+
.then()
199+
.statusCode(200)
200+
.extract().path("values.find { it.customId == 'test-pattern' }.numericId");
201+
202+
String payload = """
203+
{
204+
"type": "PATTERN",
205+
"resourceId": %d
206+
}
207+
""".formatted(numericId);
208+
209+
given()
210+
.body(payload)
211+
.header("Content-Type", "application/json")
212+
.when().put("/calm/namespaces/finos/mappings/test-pattern-alias")
213+
.then()
214+
.statusCode(201)
215+
.header("Location", containsString("/calm/namespaces/finos/mappings/test-pattern-alias/versions/"));
216+
217+
// The alias resolves to the same underlying resource
218+
given()
219+
.when().get("/calm/namespaces/finos/mappings/test-pattern-alias")
220+
.then()
221+
.statusCode(200)
222+
.body(containsString("front-controller-test"));
223+
}
224+
225+
@Test
226+
@Order(13)
227+
void link_to_nonexistent_resource_returns_404() {
228+
String payload = """
229+
{
230+
"type": "PATTERN",
231+
"resourceId": 9999
232+
}
233+
""";
234+
235+
given()
236+
.body(payload)
237+
.header("Content-Type", "application/json")
238+
.when().put("/calm/namespaces/finos/mappings/ghost-alias")
189239
.then()
190240
.statusCode(404);
191241
}

calm-hub/src/integration-test/java/integration/NitriteFrontControllerIntegration.java renamed to calm-hub/src/integration-test/java/integration/NitriteMappingControllerIntegration.java

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
@QuarkusTest
1111
@TestProfile(NitriteIntegrationTestProfile.class)
1212
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
13-
public class NitriteFrontControllerIntegration {
13+
public class NitriteMappingControllerIntegration {
1414

1515
@BeforeEach
1616
public void setup() {
@@ -30,17 +30,17 @@ void create_pattern_via_front_controller() {
3030
given()
3131
.body(payload)
3232
.header("Content-Type", "application/json")
33-
.when().post("/calm/namespaces/finos/test-pattern")
33+
.when().post("/calm/namespaces/finos/mappings/test-pattern")
3434
.then()
3535
.statusCode(201)
36-
.header("Location", containsString("/calm/namespaces/finos/test-pattern/versions/1.0.0"));
36+
.header("Location", containsString("/calm/namespaces/finos/mappings/test-pattern/versions/1.0.0"));
3737
}
3838

3939
@Test
4040
@Order(2)
4141
void get_latest_version_by_custom_id() {
4242
given()
43-
.when().get("/calm/namespaces/finos/test-pattern")
43+
.when().get("/calm/namespaces/finos/mappings/test-pattern")
4444
.then()
4545
.statusCode(200)
4646
.body(containsString("front-controller-test"));
@@ -50,7 +50,7 @@ void get_latest_version_by_custom_id() {
5050
@Order(3)
5151
void get_specific_version_by_custom_id() {
5252
given()
53-
.when().get("/calm/namespaces/finos/test-pattern/versions/1.0.0")
53+
.when().get("/calm/namespaces/finos/mappings/test-pattern/versions/1.0.0")
5454
.then()
5555
.statusCode(200)
5656
.body(containsString("front-controller-test"));
@@ -60,7 +60,7 @@ void get_specific_version_by_custom_id() {
6060
@Order(4)
6161
void list_versions_by_custom_id() {
6262
given()
63-
.when().get("/calm/namespaces/finos/test-pattern/versions")
63+
.when().get("/calm/namespaces/finos/mappings/test-pattern/versions")
6464
.then()
6565
.statusCode(200)
6666
.body("values", hasSize(1))
@@ -80,17 +80,17 @@ void update_resource_with_minor_version_bump() {
8080
given()
8181
.body(payload)
8282
.header("Content-Type", "application/json")
83-
.when().post("/calm/namespaces/finos/test-pattern")
83+
.when().post("/calm/namespaces/finos/mappings/test-pattern")
8484
.then()
8585
.statusCode(201)
86-
.header("Location", containsString("/calm/namespaces/finos/test-pattern/versions/1.1.0"));
86+
.header("Location", containsString("/calm/namespaces/finos/mappings/test-pattern/versions/1.1.0"));
8787
}
8888

8989
@Test
9090
@Order(6)
9191
void get_latest_returns_newest_version() {
9292
given()
93-
.when().get("/calm/namespaces/finos/test-pattern")
93+
.when().get("/calm/namespaces/finos/mappings/test-pattern")
9494
.then()
9595
.statusCode(200)
9696
.body(containsString("front-controller-test-v2"));
@@ -100,7 +100,7 @@ void get_latest_returns_newest_version() {
100100
@Order(7)
101101
void list_versions_shows_both() {
102102
given()
103-
.when().get("/calm/namespaces/finos/test-pattern/versions")
103+
.when().get("/calm/namespaces/finos/mappings/test-pattern/versions")
104104
.then()
105105
.statusCode(200)
106106
.body("values", hasSize(2))
@@ -122,7 +122,57 @@ void lookup_mappings_includes_created_mapping() {
122122
@Order(9)
123123
void return_404_for_nonexistent_custom_id() {
124124
given()
125-
.when().get("/calm/namespaces/finos/nonexistent-resource")
125+
.when().get("/calm/namespaces/finos/mappings/nonexistent-resource")
126+
.then()
127+
.statusCode(404);
128+
}
129+
130+
@Test
131+
@Order(10)
132+
void link_existing_resource_to_new_custom_id() {
133+
Integer numericId = given()
134+
.when().get("/calm/namespaces/finos/mappings?type=PATTERN")
135+
.then()
136+
.statusCode(200)
137+
.extract().path("values.find { it.customId == 'test-pattern' }.numericId");
138+
139+
String payload = """
140+
{
141+
"type": "PATTERN",
142+
"resourceId": %d
143+
}
144+
""".formatted(numericId);
145+
146+
given()
147+
.body(payload)
148+
.header("Content-Type", "application/json")
149+
.when().put("/calm/namespaces/finos/mappings/test-pattern-alias")
150+
.then()
151+
.statusCode(201)
152+
.header("Location", containsString("/calm/namespaces/finos/mappings/test-pattern-alias/versions/"));
153+
154+
// The alias resolves to the same underlying resource
155+
given()
156+
.when().get("/calm/namespaces/finos/mappings/test-pattern-alias")
157+
.then()
158+
.statusCode(200)
159+
.body(containsString("front-controller-test"));
160+
}
161+
162+
@Test
163+
@Order(11)
164+
void link_to_nonexistent_resource_returns_404() {
165+
String payload = """
166+
{
167+
"type": "PATTERN",
168+
"resourceId": 9999
169+
}
170+
""";
171+
172+
given()
173+
.body(payload)
174+
.header("Content-Type", "application/json")
175+
.when().put("/calm/namespaces/finos/mappings/ghost-alias")
126176
.then()
127177
.statusCode(404);
128178
}

calm-hub/src/main/java/org/finos/calm/domain/Semver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package org.finos.calm.domain;
22

3-
import org.finos.calm.domain.frontcontroller.ChangeType;
3+
import org.finos.calm.domain.mapping.ChangeType;
44

55
/**
66
* Immutable value object representing a semantic version (major.minor.patch).

calm-hub/src/main/java/org/finos/calm/domain/frontcontroller/ChangeType.java renamed to calm-hub/src/main/java/org/finos/calm/domain/mapping/ChangeType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.finos.calm.domain.frontcontroller;
1+
package org.finos.calm.domain.mapping;
22

33
public enum ChangeType {
44
MAJOR,

calm-hub/src/main/java/org/finos/calm/domain/frontcontroller/FrontControllerCreateRequest.java renamed to calm-hub/src/main/java/org/finos/calm/domain/mapping/MappingCreateRequest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
package org.finos.calm.domain.frontcontroller;
1+
package org.finos.calm.domain.mapping;
22

33
import jakarta.validation.constraints.NotBlank;
44
import java.util.Objects;
55

6-
public class FrontControllerCreateRequest {
6+
public class MappingCreateRequest {
77
@NotBlank(message = "Type must not be blank")
88
private String type;
99
@NotBlank(message = "JSON must not be blank")
1010
private String json;
1111
private String name;
1212
private String description;
1313

14-
public FrontControllerCreateRequest() {
14+
public MappingCreateRequest() {
1515
// Default constructor
1616
}
1717

18-
public FrontControllerCreateRequest(String type, String json, String name, String description) {
18+
public MappingCreateRequest(String type, String json, String name, String description) {
1919
this.type = type;
2020
this.json = json;
2121
this.name = name;
@@ -57,7 +57,7 @@ public void setDescription(String description) {
5757
@Override
5858
public boolean equals(Object o) {
5959
if (o == null || getClass() != o.getClass()) return false;
60-
FrontControllerCreateRequest that = (FrontControllerCreateRequest) o;
60+
MappingCreateRequest that = (MappingCreateRequest) o;
6161
return Objects.equals(type, that.type) && Objects.equals(json, that.json) && Objects.equals(name, that.name) && Objects.equals(description, that.description);
6262
}
6363

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.finos.calm.domain.mapping;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.NotNull;
5+
import java.util.Objects;
6+
7+
public class MappingLinkRequest {
8+
@NotBlank(message = "Type must not be blank")
9+
private String type;
10+
@NotNull(message = "Resource ID must not be null")
11+
private Integer resourceId;
12+
13+
public MappingLinkRequest() {
14+
// Default constructor
15+
}
16+
17+
public MappingLinkRequest(String type, Integer resourceId) {
18+
this.type = type;
19+
this.resourceId = resourceId;
20+
}
21+
22+
public String getType() {
23+
return type;
24+
}
25+
26+
public void setType(String type) {
27+
this.type = type;
28+
}
29+
30+
public Integer getResourceId() {
31+
return resourceId;
32+
}
33+
34+
public void setResourceId(Integer resourceId) {
35+
this.resourceId = resourceId;
36+
}
37+
38+
@Override
39+
public boolean equals(Object o) {
40+
if (o == null || getClass() != o.getClass()) return false;
41+
MappingLinkRequest that = (MappingLinkRequest) o;
42+
return Objects.equals(type, that.type) && Objects.equals(resourceId, that.resourceId);
43+
}
44+
45+
@Override
46+
public int hashCode() {
47+
return Objects.hash(type, resourceId);
48+
}
49+
}

0 commit comments

Comments
 (0)