Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pagination to bugsnag and generic api controller #72

Merged
merged 23 commits into from
Oct 12, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
68168ba
refactor(pagination): add pagination to bugsnag and generic api contr…
landonreed Sep 10, 2020
bdfc6a7
docs(swagger): update API docs
landonreed Sep 22, 2020
f6c361b
refactor(pagination): refactor ResponseList to simplify construction
landonreed Sep 28, 2020
767ee7e
docs(swagger): update swagger API docs
landonreed Sep 28, 2020
00dc9cd
refactor(bugsnag): clean up code to construct event summaries
landonreed Sep 28, 2020
efab948
Merge branch 'dev' into add-pagination
landonreed Sep 28, 2020
5c1bfcf
refactor(redirect): update HttpUtils refactored method name
landonreed Sep 28, 2020
7795bd1
test: fix broken e2e test
landonreed Sep 28, 2020
02ea4b8
refactor: add no-arg constructor for ResponseList
landonreed Sep 28, 2020
7288d2b
refactor: fix test and remove unused imports
landonreed Sep 28, 2020
a2146c3
refactor(api-key): replace customerId with tag for userId
landonreed Sep 29, 2020
755a900
refactor(pagination): change page param to offset
landonreed Sep 29, 2020
99d364b
refactor(pagination): change remaining page -> offset instances
landonreed Sep 29, 2020
af9ad88
docs(pagination): update swagger for pagination
landonreed Sep 29, 2020
affc874
refactor(HttpUtils): refactor getQueryParamFromRequest (params, javad…
landonreed Sep 30, 2020
530f20e
refactor(HttpUtils): fix max int value check
landonreed Sep 30, 2020
7e59e7a
refactor(pagination): update swagger descriptions; add ResponseList#c…
landonreed Oct 2, 2020
0c800e4
build(deps): use ibi-group fork of spark-swagger
landonreed Oct 2, 2020
44c6c77
Revert "build(deps): use ibi-group fork of spark-swagger"
landonreed Oct 12, 2020
7c1e133
docs(swagger): update API docs
landonreed Oct 12, 2020
d6e236d
Merge branch 'dev' into add-pagination
landonreed Oct 12, 2020
ace5772
test: fix trip history persistence compilation error
landonreed Oct 12, 2020
692c66a
refactor(ApiGatewayUtils): use fluent method for inlining timeout call
landonreed Oct 12, 2020
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 @@ -30,6 +30,7 @@
import java.util.concurrent.TimeUnit;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.opentripplanner.middleware.utils.HttpUtils.getQueryParamFromRequest;
import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt;

/**
Expand Down Expand Up @@ -112,7 +113,7 @@ private static void initializeHttpEndpoints() throws IOException, InterruptedExc
* _encoded_ URL e.g. http://localhost:3000/#/register which allows for greater flexibility.
*/
spark.get("/register", (request, response) -> {
String route = HttpUtils.getRequiredQueryParamFromRequest(request, "route", false);
String route = getQueryParamFromRequest(request, "route", false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using static imports like this might actually be a bad practice. See this perspective here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is your recommendation? That we qualify the method call with the util class? I'm fine with that, but we should define an approach for our projects generally on this. Perhaps we should create an issue and handle that in a separate project-wide PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss with the team together sometime and then if we decide we want to do this, then we can handle all the changes in another PR.

if (route == null) {
logMessageAndHalt(request,
HttpStatus.BAD_REQUEST_400,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

import com.beerboy.ss.ApiEndpoint;
import com.beerboy.ss.SparkSwagger;
import com.beerboy.ss.descriptor.ParameterDescriptor;
import com.beerboy.ss.rest.Endpoint;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.mongodb.client.model.Filters;
import org.bson.conversions.Bson;
import org.eclipse.jetty.http.HttpStatus;
import org.opentripplanner.middleware.auth.Auth0Connection;
import org.opentripplanner.middleware.auth.Auth0UserProfile;
import org.opentripplanner.middleware.controllers.response.ResponseList;
import org.opentripplanner.middleware.models.Model;
import org.opentripplanner.middleware.models.OtpUser;
import org.opentripplanner.middleware.persistence.TypedPersistence;
Expand All @@ -21,15 +22,14 @@
import spark.Request;
import spark.Response;

import java.lang.reflect.Array;
import java.util.Date;
import java.util.List;

import static com.beerboy.ss.descriptor.EndpointDescriptor.endpointPath;
import static com.beerboy.ss.descriptor.MethodDescriptor.path;
import static org.opentripplanner.middleware.auth.Auth0Connection.getUserFromRequest;
import static org.opentripplanner.middleware.auth.Auth0Connection.isUserAdmin;
import static org.opentripplanner.middleware.utils.HttpUtils.JSON_ONLY;
import static org.opentripplanner.middleware.utils.HttpUtils.getQueryParamFromRequest;
import static org.opentripplanner.middleware.utils.JsonUtils.getPOJOFromRequestBody;
import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt;

Expand All @@ -38,8 +38,8 @@
* methods. This will identify the MongoDB collection on which to operate based on the provided {@link Model} class.
*
* TODO: Add hooks so that validation can be performed on certain methods (e.g., validating fields on create/update,
* checking user permissions to perform certain actions, checking whether an entity can be deleted due to references
* that exist in other collection).
* checking user permissions to perform certain actions, checking whether an entity can be deleted due to references
* that exist in other collection).
*
* @param <T> One of the {@link Model} classes (extracted from {@link TypedPersistence})
*/
Expand All @@ -52,6 +52,19 @@ public abstract class ApiController<T extends Model> implements Endpoint {
private static final Logger LOG = LoggerFactory.getLogger(ApiController.class);
final TypedPersistence<T> persistence;
private final Class<T> clazz;
public static final String LIMIT_PARAM = "limit";
public static final int DEFAULT_LIMIT = 10;
public static final int DEFAULT_OFFSET = 0;
public static final String OFFSET_PARAM = "offset";

public static final ParameterDescriptor LIMIT = ParameterDescriptor.newBuilder()
.withName(LIMIT_PARAM)
.withDefaultValue(String.valueOf(DEFAULT_LIMIT))
.withDescription("If specified, the maximum number of items to return.").build();
public static final ParameterDescriptor OFFSET = ParameterDescriptor.newBuilder()
.withName(OFFSET_PARAM)
.withDefaultValue(String.valueOf(DEFAULT_OFFSET))
.withDescription("If specified, the number of records to skip/offset.").build();

/**
* @param apiPrefix string prefix to use in determining the resource location
Expand Down Expand Up @@ -112,19 +125,19 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) {

baseEndpoint
// Get multiple entities.
.get(path(ROOT_ROUTE)
.get(
path(ROOT_ROUTE)
.withDescription("Gets a list of all '" + className + "' entities.")
landonreed marked this conversation as resolved.
Show resolved Hide resolved
.withQueryParam(LIMIT)
.withQueryParam(OFFSET)
.withProduces(JSON_ONLY)
// Set the return type as the array of clazz objects.
// Note: there exists a method withResponseAsCollection, but unlike what its name suggests,
// it does exactly the same as .withResponseType and does not generate a return type array.
// See issue https://github.com/manusant/spark-swagger/issues/12.
.withResponseType(Array.newInstance(clazz, 0).getClass()),
.withResponseType(ResponseList.class),
landonreed marked this conversation as resolved.
Show resolved Hide resolved
this::getMany, JsonUtils::toJson
)

// Get one entity.
.get(path(ROOT_ROUTE + ID_PATH)
.get(
path(ROOT_ROUTE + ID_PATH)
.withDescription("Returns the '" + className + "' entity with the specified id, or 404 if not found.")
.withPathParam().withName(ID_PARAM).withRequired(true).withDescription("The id of the entity to search.").and()
// .withResponses(...) // FIXME: not implemented (requires source change).
Expand All @@ -134,7 +147,8 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) {
)

// Create entity request
.post(path("")
.post(
path("")
.withDescription("Creates a '" + className + "' entity.")
.withConsumes(JSON_ONLY)
.withRequestType(clazz)
Expand All @@ -144,7 +158,8 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) {
)

// Update entity request
.put(path(ID_PATH)
.put(
path(ID_PATH)
.withDescription("Updates and returns the '" + className + "' entity with the specified id, or 404 if not found.")
.withPathParam().withName(ID_PARAM).withRequired(true).withDescription("The id of the entity to update.").and()
.withConsumes(JSON_ONLY)
Expand All @@ -158,7 +173,8 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) {
)

// Delete entity request
.delete(path(ID_PATH)
.delete(
path(ID_PATH)
.withDescription("Deletes the '" + className + "' entity with the specified id if it exists.")
.withPathParam().withName(ID_PARAM).withRequired(true).withDescription("The id of the entity to delete.").and()
.withProduces(JSON_ONLY)
Expand All @@ -172,33 +188,26 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) {
*/
// FIXME Maybe better if the user check (and filtering) was done in a pre hook?
// FIXME Will require further granularity for admin
private List<T> getMany(Request req, Response res) {

private ResponseList<T> getMany(Request req, Response res) {
int limit = getQueryParamFromRequest(req, LIMIT_PARAM, true, 0, DEFAULT_LIMIT, 100);
int offset = getQueryParamFromRequest(req, OFFSET_PARAM, true, 0, DEFAULT_OFFSET);
Auth0UserProfile requestingUser = getUserFromRequest(req);
if (isUserAdmin(requestingUser)) {
// If the user is admin, the context is presumed to be the admin dashboard, so we deliver all entities for
// management or review without restriction.
return persistence.getAll();
return persistence.getResponseList(offset, limit);
} else if (persistence.clazz == OtpUser.class) {
// If the required entity is of type 'OtpUser' the assumption is that a call is being made via the
// OtpUserController. Therefore, the request should be limited to return just the entity matching the
// requesting user.
return getObjectsFiltered("_id", requestingUser.otpUser.id);
return persistence.getResponseList(Filters.eq("_id", requestingUser.otpUser.id), offset, limit);
} else {
// For all other cases the assumption is that the request is being made by an Otp user and the requested
// entities have a 'userId' parameter. Only entities that match the requesting user id are returned.
return getObjectsFiltered("userId", requestingUser.otpUser.id);
return persistence.getResponseList(Filters.eq("userId", requestingUser.otpUser.id), offset, limit);
}
}

/**
* Get a list of objects filtered by the provided field name and value.
*/
private List<T> getObjectsFiltered(String fieldName, String value) {
Bson filter = Filters.eq(fieldName, value);
return persistence.getFiltered(filter);
}

/**
* HTTP endpoint to get one entity specified by ID. This will return an object based on the checks carried out in
* the overridden 'canBeManagedBy' method. It is the responsibility of this method to define access to it's own
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import com.beerboy.ss.SparkSwagger;
import com.beerboy.ss.rest.Endpoint;
import com.mongodb.client.model.Filters;
import org.bson.conversions.Bson;
import com.google.common.collect.Maps;
import com.mongodb.client.FindIterable;
import com.mongodb.client.model.Sorts;
import org.opentripplanner.middleware.bugsnag.EventSummary;
import org.opentripplanner.middleware.controllers.response.ResponseList;
import org.opentripplanner.middleware.models.BugsnagEvent;
import org.opentripplanner.middleware.models.BugsnagProject;
import org.opentripplanner.middleware.persistence.Persistence;
Expand All @@ -16,10 +18,15 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static com.beerboy.ss.descriptor.EndpointDescriptor.endpointPath;
import static com.beerboy.ss.descriptor.MethodDescriptor.path;
import static org.opentripplanner.middleware.controllers.api.ApiController.DEFAULT_LIMIT;
import static org.opentripplanner.middleware.controllers.api.ApiController.LIMIT_PARAM;
import static org.opentripplanner.middleware.controllers.api.ApiController.OFFSET_PARAM;
import static org.opentripplanner.middleware.utils.HttpUtils.JSON_ONLY;
import static org.opentripplanner.middleware.utils.HttpUtils.getQueryParamFromRequest;

/**
* Responsible for providing the current set of Bugsnag events to the calling service
Expand Down Expand Up @@ -54,19 +61,24 @@ public void bind(final SparkSwagger restApi) {
}

/**
* Get all Bugsnag events from Mongo and replace the project id with the project name and return
* Get all Bugsnag events from Mongo and replace the project id with the project name and return.
landonreed marked this conversation as resolved.
Show resolved Hide resolved
*/
private static List<EventSummary> getEventSummary(Request request, Response response) {
List<EventSummary> eventSummaries = new ArrayList<>();
List<BugsnagEvent> events = bugsnagEvents.getAll();

private static ResponseList<EventSummary> getEventSummary(Request req, Response res) {
int limit = getQueryParamFromRequest(req, LIMIT_PARAM, true, 0, DEFAULT_LIMIT, 100);
int offset = getQueryParamFromRequest(req, OFFSET_PARAM, true, 0, 0);
// Get latest events from database.
FindIterable<BugsnagEvent> events = bugsnagEvents.getSortedIterableWithOffsetAndLimit(
Sorts.descending("receivedAt"),
offset,
limit
);
// Get Bugsnag projects by id (avoid multiple queries to Mongo for the same project).
Map<String, BugsnagProject> projectsById = Maps.uniqueIndex(bugsnagProjects.getAll(), p -> p.projectId);
// Construct event summaries from project map.
// FIXME: Group by error/project type?
for (BugsnagEvent event : events) {
Bson filter = Filters.eq("projectId", event.projectId);
BugsnagProject project = bugsnagProjects.getOneFiltered(filter);
eventSummaries.add(new EventSummary(project, event));
}

return eventSummaries;
List<EventSummary> eventSummaries = events
.map(event -> new EventSummary(projectsById.get(event.projectId), event))
.into(new ArrayList<>());
return new ResponseList<>(eventSummaries, offset, limit, bugsnagEvents.getCount());
}
}
Loading