Skip to content

PRO-42 Create Program Endpoints#3

Merged
nickpalladino merged 56 commits intodevelopfrom
feature/PRO-42
Mar 25, 2020
Merged

PRO-42 Create Program Endpoints#3
nickpalladino merged 56 commits intodevelopfrom
feature/PRO-42

Conversation

@nickpalladino
Copy link
Member

Includes cards PRO-42 Create program endpoints, INF-85 Create GET Species endpoints, and INF-83 Create program user endpoints. The project should build, the API documentation should be verified to be updated and in sync with the implemented endpoints, and unit tests should exist and pass that cover the implemented endpoints.

Here are the added endpoints:

GET /programs
POST /programs
GET /programs/{id}
PUT /programs/{id}
DELETE /programs/{id}

GET /species
GET /species/{id}

GET /programs/{id}/users
POST /programs/{id}/users
GET /programs/{id}/users/{userId}
PUT /programs/{id}/users/{userId}
DELETE /programs/{id}/users/{userId}

Comment on lines +34 to +55
public Species getById(UUID speciesId) throws DoesNotExistException{

SpeciesEntity speciesEntity = dao.fetchOneById(speciesId);

if (speciesEntity == null){
throw new DoesNotExistException("Species does not exist");
}

Species species = new Species(speciesEntity);
return species;
}

public Optional<SpeciesEntity> getByIdOptional(UUID speciesId) {

SpeciesEntity species = dao.fetchOneById(speciesId);

if (species == null) {
return Optional.empty();
}

return Optional.of(species);
}
Copy link
Member

@timparsons timparsons Mar 16, 2020

Choose a reason for hiding this comment

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

I'd suggest combining these methods and returning Optional<Species>

Copy link
Contributor

Choose a reason for hiding this comment

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

pushed changes

Comment on lines 57 to 78
public User getById(UUID userId) throws DoesNotExistException {

Optional<User> user = getByIdOptional(userId);

if (user.isEmpty()) {
throw new DoesNotExistException("UUID for user does not exist");
}

return user.get();
}

public Optional<User> getByIdOptional(UUID userId) {

// User has been authenticated against orcid, check they have a bi account.
BiUser biUser = dao.fetchOneById(userId);
BiUserEntity biUser = dao.fetchOneById(userId);

if (biUser == null) {
throw new DoesNotExistException("UUID for user does not exist");
return Optional.empty();
}

return new User(biUser);
return Optional.of(new User(biUser));
}
Copy link
Member

@timparsons timparsons Mar 16, 2020

Choose a reason for hiding this comment

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

I'd suggest combining these methods and returning Optional<User>

Copy link
Member Author

Choose a reason for hiding this comment

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

Pushed changes

return created.get();
}

public Optional<User> createOptional(UserRequest user) {
Copy link
Member

Choose a reason for hiding this comment

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

I'd suggest making this method private given that the create method throws an AlreadyExistsException if the user is not created, otherwise it always returns the created user.

Copy link
Member Author

Choose a reason for hiding this comment

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

pushed changes

return species;
}

public Optional<SpeciesEntity> getByIdOptional(UUID speciesId) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Should we have this return an Optional<Species> for consistency at the service interface? That was the approach I took with the ProgramUser service.

Copy link
Member

Choose a reason for hiding this comment

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

I'd say "Yes"

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 this was fixed in the commit from this morning

@Secured(SecurityRule.IS_AUTHENTICATED)
public HttpResponse<Response<ProgramUser>> updateProgramUser(Principal principal, @PathVariable UUID programId, @PathVariable UUID userId,
@Valid @Body ProgramUserRequest programUserRequest) {
/* Add a user to a program. Create the user if they don't exist. */
Copy link
Member Author

Choose a reason for hiding this comment

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

Agree, will update or remove and indicate when changes have been pushed.

import static org.breedinginsight.dao.db.Tables.*;

@Singleton
public class ProgramDao extends org.breedinginsight.dao.db.tables.daos.ProgramDao {
Copy link
Member Author

Choose a reason for hiding this comment

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

ok, don't have the same problem with the ProgramUserDao but I can rename for consistency. Will indicate when changes are pushed.

Comment on lines +20 to +25
@Inject
DSLContext dsl;
@Inject
public ProgramDao(Configuration config) {
super(config);
}
Copy link
Member Author

Choose a reason for hiding this comment

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

ok, I'll update and indicate when changes have been pushed.

SelectOnConditionStep<Record> query = dsl.select()
.from(PROGRAM)
.join(SPECIES).on(PROGRAM.SPECIES_ID.eq(SPECIES.ID))
.leftJoin(createdByUser).on(PROGRAM.CREATED_BY.eq(createdByUser.ID))
Copy link
Member Author

Choose a reason for hiding this comment

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

@ctucker3 should be fine, let me know when the db is updated and I can update mine.

user.setUser(User.parseSQLRecord(records.get(0)));

// populate roles
for (Record record : records) {
Copy link
Member Author

Choose a reason for hiding this comment

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

@timparsons What is the rationale for splitting into two database calls?

programEntity.setAbbreviation(programRequest.getAbbreviation());
programEntity.setObjective(programRequest.getObjective());
programEntity.setDocumentationUrl(programRequest.getDocumentationUrl());
programEntity.setUpdatedAt(OffsetDateTime.now());
Copy link
Member Author

Choose a reason for hiding this comment

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

sounds good

Comment on lines +67 to +72
optUser = userService.createOptional(userRequest);
if (optUser.isEmpty()) {
throw new AlreadyExistsException("Cannot create new user, email already exists");
} else {
user = optUser.get();
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Was trying to avoid having exceptions thrown that would make the code harder to follow but in this case it's not bad since there's no need for any flow control based on the exception generation, just not as explicit where the exception is coming from without looking at the called function.

Comment on lines +95 to +99
List<UUID> roleIds = programUserRequest.getRoles().stream().map(role -> role.getId()).collect(Collectors.toList());

if (hasDuplicatesRoleIds(roleIds)) {
throw new AlreadyExistsException("Duplicate roles specified, must be unique");
}
Copy link
Member Author

Choose a reason for hiding this comment

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

So you don't think we should send an error status back when the client makes a request with duplicate roles, just silently handle that as ok? If that is the case, I agree about the set, but thought that should be an error condition.

Comment on lines +101 to +108
String orcid = principal.getName();
User user = userService.getByOrcid(orcid);
Program program = programService.create(programRequest, user);
Response<Program> response = new Response(program);
return HttpResponse.ok(response);
} catch (DoesNotExistException e){
log.info(e.getMessage());
return HttpResponse.status(HttpStatus.NOT_FOUND, e.getMessage());
Copy link
Member

Choose a reason for hiding this comment

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

For the future, it would make the code cleaner to split out the try/catch blocks for service calls from different services. In this example, having the try/catch for fetching the User by their ORCID iD as a separate block from creating the program would make it clearer that the DoesNotExistException is thrown by UserService#getByOrcid and not by ProgramService#create

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed

Copy link
Member Author

Choose a reason for hiding this comment

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

1 suggested change (having UserService#fetchByOrcid return Optional<User>) that should be addressed before merge. Left a few other comments for general discussion, but not blocking of approval of the merge. I'm going to approve now even with the suggested change.

Added card as discussed.

private UserDao dao;
private UserDAO dao;

public User getByOrcid(String orcid) throws DoesNotExistException {
Copy link
Member

Choose a reason for hiding this comment

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

I think this should return Optional<User> just as getById does

Copy link
Member Author

Choose a reason for hiding this comment

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

Added card as discussed.

Comment on lines +43 to +51
public List<Role> getRolesByIds(List<UUID> roleIds) {
List<RoleEntity> roleEntities = dao.fetchById(roleIds.stream().toArray(UUID[]::new));

List<Role> roles = new ArrayList<>();
for (RoleEntity roleEntity: roleEntities){
roles.add(new Role(roleEntity));
}
return roles;
}
Copy link
Member

Choose a reason for hiding this comment

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

What if a role id doesn't exist? Should the service verify that and notify the caller if the list of roles returned from the DB doesn't contain all the roles requested by ID?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is currently checked by the only caller in ProgramUserService#validateAndGetRoles. In the future if this method is used by other callers we could evaluate whether the logic should be moved into the RoleService or by the callers.

insert into bi_user (orcid, name, email) values ('0000-0002-7156-4503', 'Nick Palladino', 'nicksandbox@mailinator.com');
insert into bi_user (orcid, name) values ('0000-0003-0437-8310', 'BI-DEV Admin');
insert into bi_user (id, orcid, name, email) values ('74a6ebfe-d114-419b-8bdc-2f7b52d26172', '1111-2222-3333-4444', 'Test User', 'test@test.com'); No newline at end of file
insert into bi_user (id, orcid, name, created_by, updated_by)
Copy link
Member

Choose a reason for hiding this comment

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

Is it worth having a "system user" that's not a real person? I.e. "BI-Admin"

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah good question. For now it is a placeholder, we haven't really discussed what the start up of the application looks like for other groups, and I imagine that this starting user is going to be in that discussion.

I could see an admin user that is the first user in any system that gets spun up. In the test isolation task this user is made configurable by making the admin orcid an environmental variable. We will need at least one user in the system with system admin permissions starting out to get your programs and users created.

Copy link
Member

@timparsons timparsons left a comment

Choose a reason for hiding this comment

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

1 suggested change (having UserService#fetchByOrcid return Optional<User>) that should be addressed before merge. Left a few other comments for general discussion, but not blocking of approval of the merge. I'm going to approve now even with the suggested change.

@nickpalladino nickpalladino merged commit 6f2a522 into develop Mar 25, 2020
@nickpalladino nickpalladino deleted the feature/PRO-42 branch March 25, 2020 16:20
timparsons pushed a commit that referenced this pull request Oct 9, 2020
PRO-31 Log in via ORCID + table lookup

Approved-by: Liz Woods
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants