Skip to content

Commit 9504af5

Browse files
committed
feat: add simple feature flags to consent
1 parent ff33e56 commit 9504af5

File tree

8 files changed

+426
-0
lines changed

8 files changed

+426
-0
lines changed

src/main/java/org/broadinstitute/consent/http/ConsentApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import org.broadinstitute.consent.http.resources.DraftResource;
6161
import org.broadinstitute.consent.http.resources.EmailNotifierResource;
6262
import org.broadinstitute.consent.http.resources.ErrorResource;
63+
import org.broadinstitute.consent.http.resources.FeatureFlagResource;
6364
import org.broadinstitute.consent.http.resources.InstitutionResource;
6465
import org.broadinstitute.consent.http.resources.LibraryCardResource;
6566
import org.broadinstitute.consent.http.resources.LivenessResource;
@@ -167,6 +168,7 @@ public void run(ConsentConfiguration config, Environment env) {
167168
env.jersey().register(injector.getInstance(DatasetResource.class));
168169
env.jersey().register(injector.getInstance(DraftResource.class));
169170
env.jersey().register(injector.getInstance(EmailNotifierResource.class));
171+
env.jersey().register(injector.getInstance(FeatureFlagResource.class));
170172
env.jersey().register(injector.getInstance(InstitutionResource.class));
171173
env.jersey().register(injector.getInstance(LibraryCardResource.class));
172174
env.jersey().register(injector.getInstance(LivenessResource.class));
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package org.broadinstitute.consent.http.db;
2+
3+
import java.util.List;
4+
import org.broadinstitute.consent.http.db.mapper.FeatureFlagMapper;
5+
import org.broadinstitute.consent.http.models.FeatureFlag;
6+
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
7+
import org.jdbi.v3.sqlobject.customizer.Bind;
8+
import org.jdbi.v3.sqlobject.statement.SqlQuery;
9+
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
10+
import org.jdbi.v3.sqlobject.transaction.Transactional;
11+
12+
@RegisterRowMapper(FeatureFlagMapper.class)
13+
public interface FeatureFlagDAO extends Transactional<FeatureFlagDAO> {
14+
15+
/**
16+
* Find all feature flags
17+
*
18+
* @return List<FeatureFlag>
19+
*/
20+
@SqlQuery("SELECT id, value, create_date, update_date FROM feature_flag ORDER BY id")
21+
List<FeatureFlag> findAll();
22+
23+
/**
24+
* Find a feature flag by id
25+
*
26+
* @param id The feature flag id
27+
* @return FeatureFlag or null if not found
28+
*/
29+
@SqlQuery(
30+
"SELECT id, value, create_date, update_date FROM feature_flag WHERE id = :id")
31+
FeatureFlag findById(@Bind("id") String id);
32+
33+
/**
34+
* Insert a new feature flag
35+
*
36+
* @param id The feature flag id
37+
* @param value The feature flag value
38+
*/
39+
@SqlUpdate(
40+
"""
41+
INSERT INTO feature_flag (id, value, create_date, update_date)
42+
VALUES (:id, :value, NOW(), NOW())
43+
""")
44+
void insert(@Bind("id") String id, @Bind("value") String value);
45+
46+
/**
47+
* Update an existing feature flag
48+
*
49+
* @param id The feature flag id
50+
* @param value The new feature flag value
51+
*/
52+
@SqlUpdate("UPDATE feature_flag SET value = :value, update_date = NOW() WHERE id = :id")
53+
void update(@Bind("id") String id, @Bind("value") String value);
54+
55+
/**
56+
* Delete a feature flag by id
57+
*
58+
* @param id The feature flag id
59+
*/
60+
@SqlUpdate("DELETE FROM feature_flag WHERE id = :id")
61+
void deleteById(@Bind("id") String id);
62+
63+
/**
64+
* Check if a feature flag exists
65+
*
66+
* @param id The feature flag id
67+
* @return true if exists, false otherwise
68+
*/
69+
@SqlQuery("SELECT COUNT(*) > 0 FROM feature_flag WHERE id = :id")
70+
boolean exists(@Bind("id") String id);
71+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.broadinstitute.consent.http.db.mapper;
2+
3+
import java.sql.ResultSet;
4+
import java.sql.SQLException;
5+
import org.broadinstitute.consent.http.models.FeatureFlag;
6+
import org.jdbi.v3.core.mapper.RowMapper;
7+
import org.jdbi.v3.core.statement.StatementContext;
8+
9+
public class FeatureFlagMapper implements RowMapper<FeatureFlag> {
10+
11+
@Override
12+
public FeatureFlag map(ResultSet rs, StatementContext ctx) throws SQLException {
13+
return new FeatureFlag(
14+
rs.getString("id"),
15+
rs.getString("value"),
16+
rs.getTimestamp("create_date").toInstant(),
17+
rs.getTimestamp("update_date").toInstant());
18+
}
19+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.broadinstitute.consent.http.models;
2+
3+
import java.time.Instant;
4+
5+
public class FeatureFlag {
6+
7+
private String id;
8+
private String value;
9+
private Instant createDate;
10+
private Instant updateDate;
11+
12+
public FeatureFlag() {}
13+
14+
public FeatureFlag(String id, String value) {
15+
this.id = id;
16+
this.value = value;
17+
}
18+
19+
public FeatureFlag(String id, String value, Instant createDate, Instant updateDate) {
20+
this.id = id;
21+
this.value = value;
22+
this.createDate = createDate;
23+
this.updateDate = updateDate;
24+
}
25+
26+
public String getId() {
27+
return id;
28+
}
29+
30+
public void setId(String id) {
31+
this.id = id;
32+
}
33+
34+
public String getValue() {
35+
return value;
36+
}
37+
38+
public void setValue(String value) {
39+
this.value = value;
40+
}
41+
42+
public Instant getCreateDate() {
43+
return createDate;
44+
}
45+
46+
public void setCreateDate(Instant createDate) {
47+
this.createDate = createDate;
48+
}
49+
50+
public Instant getUpdateDate() {
51+
return updateDate;
52+
}
53+
54+
public void setUpdateDate(Instant updateDate) {
55+
this.updateDate = updateDate;
56+
}
57+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package org.broadinstitute.consent.http.resources;
2+
3+
import com.google.inject.Inject;
4+
import io.dropwizard.auth.Auth;
5+
import jakarta.annotation.security.PermitAll;
6+
import jakarta.annotation.security.RolesAllowed;
7+
import jakarta.ws.rs.Consumes;
8+
import jakarta.ws.rs.DELETE;
9+
import jakarta.ws.rs.GET;
10+
import jakarta.ws.rs.POST;
11+
import jakarta.ws.rs.Path;
12+
import jakarta.ws.rs.PathParam;
13+
import jakarta.ws.rs.Produces;
14+
import jakarta.ws.rs.core.Context;
15+
import jakarta.ws.rs.core.MediaType;
16+
import jakarta.ws.rs.core.Response;
17+
import jakarta.ws.rs.core.UriInfo;
18+
import java.net.URI;
19+
import java.util.List;
20+
import java.util.Map;
21+
import org.broadinstitute.consent.http.models.AuthUser;
22+
import org.broadinstitute.consent.http.models.FeatureFlag;
23+
import org.broadinstitute.consent.http.service.FeatureFlagService;
24+
25+
@Path("api/feature")
26+
public class FeatureFlagResource extends Resource {
27+
28+
private final FeatureFlagService featureFlagService;
29+
30+
@Inject
31+
public FeatureFlagResource(FeatureFlagService featureFlagService) {
32+
this.featureFlagService = featureFlagService;
33+
}
34+
35+
/**
36+
* Get all feature flags (unauthenticated)
37+
*
38+
* @return List of all feature flags
39+
*/
40+
@GET
41+
@Produces(MediaType.APPLICATION_JSON)
42+
@PermitAll
43+
public Response getAllFeatureFlags() {
44+
try {
45+
List<FeatureFlag> flags = featureFlagService.getAllFeatureFlags();
46+
return Response.ok(flags).build();
47+
} catch (Exception e) {
48+
return createExceptionResponse(e);
49+
}
50+
}
51+
52+
/**
53+
* Get a specific feature flag by id (unauthenticated)
54+
*
55+
* @param id The feature flag id
56+
* @return The feature flag
57+
*/
58+
@GET
59+
@Path("/{id}")
60+
@Produces(MediaType.APPLICATION_JSON)
61+
@PermitAll
62+
public Response getFeatureFlagById(@PathParam("id") String id) {
63+
try {
64+
FeatureFlag flag = featureFlagService.getFeatureFlagById(id);
65+
return Response.ok(flag).build();
66+
} catch (Exception e) {
67+
return createExceptionResponse(e);
68+
}
69+
}
70+
71+
/**
72+
* Create or update a feature flag (admin only)
73+
*
74+
* @param info URI information
75+
* @param authUser The authenticated user
76+
* @param id The feature flag id
77+
* @param body Request body containing the value
78+
* @return The created or updated feature flag
79+
*/
80+
@POST
81+
@Path("/{id}")
82+
@Consumes(MediaType.APPLICATION_JSON)
83+
@Produces(MediaType.APPLICATION_JSON)
84+
@RolesAllowed({ADMIN})
85+
public Response createOrUpdateFeatureFlag(
86+
@Context UriInfo info,
87+
@Auth AuthUser authUser,
88+
@PathParam("id") String id,
89+
Map<String, String> body) {
90+
try {
91+
String value = body.get("value");
92+
if (value == null) {
93+
return Response.status(Response.Status.BAD_REQUEST)
94+
.entity(Map.of("error", "Missing 'value' in request body"))
95+
.build();
96+
}
97+
98+
boolean existed = featureFlagService.exists(id);
99+
FeatureFlag flag = featureFlagService.createOrUpdateFeatureFlag(id, value);
100+
101+
if (existed) {
102+
return Response.ok(flag).build();
103+
} else {
104+
URI uri = info.getAbsolutePathBuilder().path(id).build();
105+
return Response.created(uri).entity(flag).build();
106+
}
107+
} catch (Exception e) {
108+
return createExceptionResponse(e);
109+
}
110+
}
111+
112+
/**
113+
* Delete a feature flag (admin only)
114+
*
115+
* @param authUser The authenticated user
116+
* @param id The feature flag id
117+
* @return No content response
118+
*/
119+
@DELETE
120+
@Path("/{id}")
121+
@RolesAllowed({ADMIN})
122+
public Response deleteFeatureFlag(@Auth AuthUser authUser, @PathParam("id") String id) {
123+
try {
124+
featureFlagService.deleteFeatureFlag(id);
125+
return Response.noContent().build();
126+
} catch (Exception e) {
127+
return createExceptionResponse(e);
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)