Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import org.broadinstitute.consent.http.resources.DraftResource;
import org.broadinstitute.consent.http.resources.EmailNotifierResource;
import org.broadinstitute.consent.http.resources.ErrorResource;
import org.broadinstitute.consent.http.resources.FeatureFlagResource;
import org.broadinstitute.consent.http.resources.InstitutionResource;
import org.broadinstitute.consent.http.resources.LibraryCardResource;
import org.broadinstitute.consent.http.resources.LivenessResource;
Expand Down Expand Up @@ -167,6 +168,7 @@ public void run(ConsentConfiguration config, Environment env) {
env.jersey().register(injector.getInstance(DatasetResource.class));
env.jersey().register(injector.getInstance(DraftResource.class));
env.jersey().register(injector.getInstance(EmailNotifierResource.class));
env.jersey().register(injector.getInstance(FeatureFlagResource.class));
env.jersey().register(injector.getInstance(InstitutionResource.class));
env.jersey().register(injector.getInstance(LibraryCardResource.class));
env.jersey().register(injector.getInstance(LivenessResource.class));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.broadinstitute.consent.http.db.DatasetDAO;
import org.broadinstitute.consent.http.db.DraftDAO;
import org.broadinstitute.consent.http.db.ElectionDAO;
import org.broadinstitute.consent.http.db.FeatureFlagDAO;
import org.broadinstitute.consent.http.db.FileStorageObjectDAO;
import org.broadinstitute.consent.http.db.InstitutionDAO;
import org.broadinstitute.consent.http.db.LibraryCardDAO;
Expand All @@ -57,6 +58,7 @@
import org.broadinstitute.consent.http.service.ElasticSearchService;
import org.broadinstitute.consent.http.service.ElectionService;
import org.broadinstitute.consent.http.service.EmailService;
import org.broadinstitute.consent.http.service.FeatureFlagService;
import org.broadinstitute.consent.http.service.FileStorageObjectService;
import org.broadinstitute.consent.http.service.InstitutionService;
import org.broadinstitute.consent.http.service.LibraryCardService;
Expand Down Expand Up @@ -119,6 +121,7 @@ public class ConsentModule extends AbstractModule {
private final AcknowledgementDAO acknowledgementDAO;
private final DraftDAO draftDAO;
private final DACAutomationRuleDAO rulesDAO;
private final FeatureFlagDAO featureFlagDAO;

ConsentModule(ConsentConfiguration consentConfiguration, Environment environment) {
this.config = consentConfiguration;
Expand Down Expand Up @@ -156,6 +159,7 @@ public class ConsentModule extends AbstractModule {
this.acknowledgementDAO = this.jdbi.onDemand((AcknowledgementDAO.class));
this.draftDAO = this.jdbi.onDemand(DraftDAO.class);
this.rulesDAO = this.jdbi.onDemand(DACAutomationRuleDAO.class);
this.featureFlagDAO = this.jdbi.onDemand(FeatureFlagDAO.class);
}

@Override
Expand Down Expand Up @@ -350,6 +354,11 @@ EmailService providesEmailService() {
config);
}

@Provides
FeatureFlagService providesFeatureFlagService() {
return new FeatureFlagService(providesFeatureFlagDAO());
}

@Provides
SendGridAPI providesSendGridAPI() {
return new SendGridAPI(config.getMailConfiguration(), providesUserDAO());
Expand Down Expand Up @@ -679,4 +688,9 @@ DraftServiceDAO providesDraftService() {
DACAutomationRuleDAO providesDACAutomationRuleDAO() {
return rulesDAO;
}

@Provides
FeatureFlagDAO providesFeatureFlagDAO() {
return featureFlagDAO;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.broadinstitute.consent.http.db;

import java.util.List;
import org.broadinstitute.consent.http.db.mapper.FeatureFlagMapper;
import org.broadinstitute.consent.http.models.FeatureFlag;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.customizer.Bind;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import org.jdbi.v3.sqlobject.transaction.Transactional;

@RegisterRowMapper(FeatureFlagMapper.class)
public interface FeatureFlagDAO extends Transactional<FeatureFlagDAO> {

/**
* Find all feature flags
*
* @return List<FeatureFlag>
*/
@SqlQuery("SELECT id, value, create_date, update_date FROM feature_flag ORDER BY id")
List<FeatureFlag> findAll();

/**
* Find a feature flag by id
*
* @param id The feature flag id
* @return FeatureFlag or null if not found
*/
@SqlQuery("SELECT id, value, create_date, update_date FROM feature_flag WHERE id = :id")
FeatureFlag findById(@Bind("id") String id);

/**
* Insert a new feature flag
*
* @param id The feature flag id
* @param value The feature flag value
*/
@SqlUpdate(
"""
INSERT INTO feature_flag (id, value, create_date, update_date)
VALUES (:id, :value, NOW(), NOW())
""")
void insert(@Bind("id") String id, @Bind("value") String value);

/**
* Update an existing feature flag
*
* @param id The feature flag id
* @param value The new feature flag value
*/
@SqlUpdate("UPDATE feature_flag SET value = :value, update_date = NOW() WHERE id = :id")
void update(@Bind("id") String id, @Bind("value") String value);

/**
* Delete a feature flag by id
*
* @param id The feature flag id
*/
@SqlUpdate("DELETE FROM feature_flag WHERE id = :id")
void deleteById(@Bind("id") String id);

/**
* Check if a feature flag exists
*
* @param id The feature flag id
* @return true if exists, false otherwise
*/
@SqlQuery("SELECT COUNT(*) > 0 FROM feature_flag WHERE id = :id")
boolean exists(@Bind("id") String id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.broadinstitute.consent.http.db.mapper;

import java.sql.ResultSet;
import java.sql.SQLException;
import org.broadinstitute.consent.http.models.FeatureFlag;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;

public class FeatureFlagMapper implements RowMapper<FeatureFlag> {

@Override
public FeatureFlag map(ResultSet rs, StatementContext ctx) throws SQLException {
return new FeatureFlag(
rs.getString("id"),
rs.getString("value"),
rs.getTimestamp("create_date").toInstant(),
rs.getTimestamp("update_date").toInstant());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.broadinstitute.consent.http.models;

import java.time.Instant;

public class FeatureFlag {

private String id;
private String value;
private Instant createDate;
private Instant updateDate;

public FeatureFlag() {}

public FeatureFlag(String id, String value) {
this.id = id;
this.value = value;
}

public FeatureFlag(String id, String value, Instant createDate, Instant updateDate) {
this.id = id;
this.value = value;
this.createDate = createDate;
this.updateDate = updateDate;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getValue() {
return value;
}

public void setValue(String value) {
this.value = value;
}

public Instant getCreateDate() {
return createDate;
}

public void setCreateDate(Instant createDate) {
this.createDate = createDate;
}

public Instant getUpdateDate() {
return updateDate;
}

public void setUpdateDate(Instant updateDate) {
this.updateDate = updateDate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package org.broadinstitute.consent.http.resources;

import com.google.inject.Inject;
import io.dropwizard.auth.Auth;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.List;
import java.util.Map;
import org.broadinstitute.consent.http.models.AuthUser;
import org.broadinstitute.consent.http.models.FeatureFlag;
import org.broadinstitute.consent.http.service.FeatureFlagService;

@Path("api/feature")
public class FeatureFlagResource extends Resource {

private final FeatureFlagService featureFlagService;

@Inject
public FeatureFlagResource(FeatureFlagService featureFlagService) {
this.featureFlagService = featureFlagService;
}

/**
* Get all feature flags
*
* @return List of all feature flags
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
@PermitAll
public Response getAllFeatureFlags() {
try {
List<FeatureFlag> flags = featureFlagService.getAllFeatureFlags();
return Response.ok(flags).build();
} catch (Exception e) {
return createExceptionResponse(e);
}
}

/**
* Get a specific feature flag by id
*
* @param id The feature flag id
* @return The feature flag
*/
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
@PermitAll
public Response getFeatureFlagById(@PathParam("id") String id) {
try {
FeatureFlag flag = featureFlagService.getFeatureFlagById(id);
return Response.ok(flag).build();
} catch (Exception e) {
return createExceptionResponse(e);
}
}

/**
* Create or update a feature flag (admin only)
*
* @param info URI information
* @param authUser The authenticated user
* @param id The feature flag id
* @param body Request body containing the value
* @return The created or updated feature flag
*/
@POST
@Path("/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed({ADMIN})
public Response createOrUpdateFeatureFlag(
@Context UriInfo info,
@Auth AuthUser authUser,
@PathParam("id") String id,
Map<String, String> body) {
try {
String value = body.get("value");
if (value == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Missing 'value' in request body"))
.build();
}

boolean existed = featureFlagService.exists(id);
FeatureFlag flag = featureFlagService.createOrUpdateFeatureFlag(id, value);

if (existed) {
return Response.ok(flag).build();
} else {
URI uri = info.getAbsolutePathBuilder().path(id).build();
return Response.created(uri).entity(flag).build();
}
} catch (Exception e) {
return createExceptionResponse(e);
}
}

/**
* Delete a feature flag (admin only)
*
* @param authUser The authenticated user
* @param id The feature flag id
* @return No content response
*/
@DELETE
@Path("/{id}")
@RolesAllowed({ADMIN})
public Response deleteFeatureFlag(@Auth AuthUser authUser, @PathParam("id") String id) {
try {
featureFlagService.deleteFeatureFlag(id);
return Response.noContent().build();
} catch (Exception e) {
return createExceptionResponse(e);
}
}
}
Loading
Loading