diff --git a/.run/Main.run.xml b/.run/Main.run.xml new file mode 100644 index 000000000..f7add6ea3 --- /dev/null +++ b/.run/Main.run.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/service/src/main/java/bio/terra/tanagra/app/controller/CohortsV2ApiController.java b/service/src/main/java/bio/terra/tanagra/app/controller/CohortsV2ApiController.java index 1d4e1dfd1..bc58425d4 100644 --- a/service/src/main/java/bio/terra/tanagra/app/controller/CohortsV2ApiController.java +++ b/service/src/main/java/bio/terra/tanagra/app/controller/CohortsV2ApiController.java @@ -70,6 +70,7 @@ public ResponseEntity createCohort(String studyId, ApiCohortCreateI .underlayName(body.getUnderlayName()) .cohortRevisionGroupId(newCohortRevisionGroupId) .version(Cohort.STARTING_VERSION) + .createdBy(UserId.currentUser().getEmail()) .displayName(body.getDisplayName()) .description(body.getDescription()) .build(); diff --git a/service/src/main/java/bio/terra/tanagra/app/controller/ConceptSetsV2ApiController.java b/service/src/main/java/bio/terra/tanagra/app/controller/ConceptSetsV2ApiController.java index 7eeb8970a..268c4036b 100644 --- a/service/src/main/java/bio/terra/tanagra/app/controller/ConceptSetsV2ApiController.java +++ b/service/src/main/java/bio/terra/tanagra/app/controller/ConceptSetsV2ApiController.java @@ -67,6 +67,7 @@ public ResponseEntity createConceptSet( .conceptSetId(newConceptSetId) .underlayName(body.getUnderlayName()) .entityName(body.getEntity()) + .createdBy(UserId.currentUser().getEmail()) .displayName(body.getDisplayName()) .description(body.getDescription()) .build(); @@ -153,7 +154,6 @@ public ResponseEntity updateConceptSet( return ResponseEntity.ok(toApiObject(updatedConceptSet)); } - /** Convert the internal Concept Set object to an API Concept Set object. */ private static ApiConceptSetV2 toApiObject(ConceptSet conceptSet) { return new ApiConceptSetV2() .id(conceptSet.getConceptSetId()) @@ -161,7 +161,9 @@ private static ApiConceptSetV2 toApiObject(ConceptSet conceptSet) { .entity(conceptSet.getEntityName()) .displayName(conceptSet.getDisplayName()) .description(conceptSet.getDescription()) - .lastModified(conceptSet.getLastModifiedUTC()) + .created(conceptSet.getCreated()) + .createdBy(conceptSet.getCreatedBy()) + .lastModified(conceptSet.getLastModified()) .criteria( conceptSet.getCriteria() == null ? null diff --git a/service/src/main/java/bio/terra/tanagra/app/controller/ReviewsV2ApiController.java b/service/src/main/java/bio/terra/tanagra/app/controller/ReviewsV2ApiController.java index db378278b..758ae905c 100644 --- a/service/src/main/java/bio/terra/tanagra/app/controller/ReviewsV2ApiController.java +++ b/service/src/main/java/bio/terra/tanagra/app/controller/ReviewsV2ApiController.java @@ -106,6 +106,7 @@ public ResponseEntity createReview( .displayName(body.getDisplayName()) .description(body.getDescription()) .size(body.getSize()) + .createdBy(UserId.currentUser().getEmail()) .build(); // TODO: Move this to the ReviewService once we can build the EntityFilter from the Cohort on @@ -309,7 +310,9 @@ private static ApiReviewV2 toApiObject(Review review) { .displayName(review.getDisplayName()) .description(review.getDescription()) .size(review.getSize()) - .created(review.getCreatedUTC()) + .created(review.getCreated()) + .createdBy(review.getCreatedBy()) + .lastModified(review.getLastModified()) .cohort(ToApiConversionUtils.toApiObject(review.getCohort())); } diff --git a/service/src/main/java/bio/terra/tanagra/app/controller/StudiesV2ApiController.java b/service/src/main/java/bio/terra/tanagra/app/controller/StudiesV2ApiController.java index 171e2c669..600c99ff3 100644 --- a/service/src/main/java/bio/terra/tanagra/app/controller/StudiesV2ApiController.java +++ b/service/src/main/java/bio/terra/tanagra/app/controller/StudiesV2ApiController.java @@ -55,6 +55,7 @@ public ResponseEntity createStudy(ApiStudyCreateInfoV2 body) { .displayName(body.getDisplayName()) .description(body.getDescription()) .properties(fromApiObject(body.getProperties())) + .createdBy(UserId.currentUser().getEmail()) .build(); studyService.createStudy(studyToCreate); return ResponseEntity.ok(toApiObject(studyToCreate)); @@ -133,7 +134,10 @@ private static ApiStudyV2 toApiObject(Study study) { .id(study.getStudyId()) .displayName(study.getDisplayName()) .description(study.getDescription()) - .properties(apiProperties); + .properties(apiProperties) + .created(study.getCreated()) + .createdBy(study.getCreatedBy()) + .lastModified(study.getLastModified()); } private static ImmutableMap fromApiObject( diff --git a/service/src/main/java/bio/terra/tanagra/db/CohortDao.java b/service/src/main/java/bio/terra/tanagra/db/CohortDao.java index 15fcb4d5c..598efb7e3 100644 --- a/service/src/main/java/bio/terra/tanagra/db/CohortDao.java +++ b/service/src/main/java/bio/terra/tanagra/db/CohortDao.java @@ -38,7 +38,7 @@ public class CohortDao { // SQL query and row mapper for reading a cohort. private static final String COHORT_SELECT_SQL = - "SELECT study_id, cohort_id, underlay_name, cohort_revision_group_id, version, is_most_recent, is_editable, last_modified, display_name, description FROM cohort"; + "SELECT study_id, cohort_id, underlay_name, cohort_revision_group_id, version, is_most_recent, is_editable, created, created_by, last_modified, display_name, description FROM cohort"; private static final RowMapper COHORT_ROW_MAPPER = (rs, rowNum) -> Cohort.builder() @@ -49,7 +49,9 @@ public class CohortDao { .version(rs.getInt("version")) .isMostRecent(rs.getBoolean("is_most_recent")) .isEditable(rs.getBoolean("is_editable")) - .lastModified(rs.getTimestamp("last_modified")) + .createdBy(rs.getString("created_by")) + .created(DbUtils.timestampToOffsetDateTime(rs.getTimestamp("created"))) + .lastModified(DbUtils.timestampToOffsetDateTime(rs.getTimestamp("last_modified"))) .displayName(rs.getString("display_name")) .description(rs.getString("description")); @@ -342,8 +344,8 @@ public boolean updateCohortLatestVersion( private void createCohortHelper(Cohort cohort) { // Store the cohort. New cohort rows are always the most recent and editable. final String cohortSql = - "INSERT INTO cohort (study_id, cohort_id, underlay_name, cohort_revision_group_id, version, is_most_recent, is_editable, last_modified, display_name, description) " - + "VALUES (:study_id, :cohort_id, :underlay_name, :cohort_revision_group_id, :version, TRUE, TRUE, :last_modified, :display_name, :description)"; + "INSERT INTO cohort (study_id, cohort_id, underlay_name, cohort_revision_group_id, version, is_most_recent, is_editable, created_by, last_modified, display_name, description) " + + "VALUES (:study_id, :cohort_id, :underlay_name, :cohort_revision_group_id, :version, TRUE, TRUE, :created_by, :last_modified, :display_name, :description)"; MapSqlParameterSource params = new MapSqlParameterSource() .addValue("study_id", cohort.getStudyId()) @@ -351,6 +353,8 @@ private void createCohortHelper(Cohort cohort) { .addValue("underlay_name", cohort.getUnderlayName()) .addValue("cohort_revision_group_id", cohort.getCohortRevisionGroupId()) .addValue("version", cohort.getVersion()) + // Don't need to set created. Liquibase defaultValueComputed handles that. + .addValue("created_by", cohort.getCreatedBy()) .addValue("last_modified", Timestamp.from(Instant.now())) .addValue("display_name", cohort.getDisplayName()) .addValue("description", cohort.getDescription()); diff --git a/service/src/main/java/bio/terra/tanagra/db/ConceptSetDao.java b/service/src/main/java/bio/terra/tanagra/db/ConceptSetDao.java index 9d854b95b..0400d26d3 100644 --- a/service/src/main/java/bio/terra/tanagra/db/ConceptSetDao.java +++ b/service/src/main/java/bio/terra/tanagra/db/ConceptSetDao.java @@ -35,7 +35,7 @@ public class ConceptSetDao { // SQL query and row mapper for reading a concept set. private static final String CONCEPT_SET_SELECT_SQL = - "SELECT study_id, concept_set_id, underlay_name, entity_name, last_modified, display_name, description FROM concept_set"; + "SELECT study_id, concept_set_id, underlay_name, entity_name, created, created_by, last_modified, display_name, description FROM concept_set"; private static final RowMapper CONCEPT_SET_ROW_MAPPER = (rs, rowNum) -> ConceptSet.builder() @@ -43,7 +43,9 @@ public class ConceptSetDao { .conceptSetId(rs.getString("concept_set_id")) .underlayName(rs.getString("underlay_name")) .entityName(rs.getString("entity_name")) - .lastModified(rs.getTimestamp("last_modified")) + .created(DbUtils.timestampToOffsetDateTime(rs.getTimestamp("created"))) + .createdBy(rs.getString("created_by")) + .lastModified(DbUtils.timestampToOffsetDateTime(rs.getTimestamp("last_modified"))) .displayName(rs.getString("display_name")) .description(rs.getString("description")); @@ -170,14 +172,16 @@ private void populateCriteria(List conceptSets) { public void createConceptSet(ConceptSet conceptSet) { // Store the concept set. final String sql = - "INSERT INTO concept_set (study_id, concept_set_id, underlay_name, entity_name, last_modified, display_name, description) " - + "VALUES (:study_id, :concept_set_id, :underlay_name, :entity_name, :last_modified, :display_name, :description)"; + "INSERT INTO concept_set (study_id, concept_set_id, underlay_name, entity_name, created_by, last_modified, display_name, description) " + + "VALUES (:study_id, :concept_set_id, :underlay_name, :entity_name, :created_by, :last_modified, :display_name, :description)"; MapSqlParameterSource params = new MapSqlParameterSource() .addValue("study_id", conceptSet.getStudyId()) .addValue("concept_set_id", conceptSet.getConceptSetId()) .addValue("underlay_name", conceptSet.getUnderlayName()) .addValue("entity_name", conceptSet.getEntityName()) + // Don't need to set created. Liquibase defaultValueComputed handles that. + .addValue("created_by", conceptSet.getCreatedBy()) .addValue("last_modified", Timestamp.from(Instant.now())) .addValue("display_name", conceptSet.getDisplayName()) .addValue("description", conceptSet.getDescription()); diff --git a/service/src/main/java/bio/terra/tanagra/db/DbUtils.java b/service/src/main/java/bio/terra/tanagra/db/DbUtils.java index 134bcc418..57660bb16 100644 --- a/service/src/main/java/bio/terra/tanagra/db/DbUtils.java +++ b/service/src/main/java/bio/terra/tanagra/db/DbUtils.java @@ -1,6 +1,9 @@ package bio.terra.tanagra.db; import bio.terra.common.exception.MissingRequiredFieldException; +import java.sql.Timestamp; +import java.time.OffsetDateTime; +import java.time.ZoneId; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -39,4 +42,8 @@ public static String setColumnsClause(MapSqlParameterSource columnParams, String return sb.toString(); } + + public static OffsetDateTime timestampToOffsetDateTime(Timestamp timestamp) { + return OffsetDateTime.ofInstant(timestamp.toInstant(), ZoneId.of("UTC")); + } } diff --git a/service/src/main/java/bio/terra/tanagra/db/ReviewDao.java b/service/src/main/java/bio/terra/tanagra/db/ReviewDao.java index f0caf1eac..086963334 100644 --- a/service/src/main/java/bio/terra/tanagra/db/ReviewDao.java +++ b/service/src/main/java/bio/terra/tanagra/db/ReviewDao.java @@ -10,8 +10,6 @@ import bio.terra.tanagra.query.RowResult; import bio.terra.tanagra.service.artifact.Cohort; import bio.terra.tanagra.service.artifact.Review; -import java.sql.Timestamp; -import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -37,7 +35,7 @@ public class ReviewDao { // SQL query and row mapper for reading a review. private static final String REVIEW_SELECT_SQL = - "SELECT r.cohort_id, r.review_id, r.display_name, r.description, r.size, r.created FROM review AS r " + "SELECT r.cohort_id, r.review_id, r.display_name, r.description, r.size, r.created, r.created_by, r.last_modified FROM review AS r " + "JOIN cohort AS c ON c.cohort_id = r.cohort_id"; private static final RowMapper REVIEW_ROW_MAPPER = (rs, rowNum) -> @@ -47,7 +45,9 @@ public class ReviewDao { .displayName(rs.getString("display_name")) .description(rs.getString("description")) .size(rs.getInt("size")) - .created(rs.getTimestamp("created")); + .created(DbUtils.timestampToOffsetDateTime(rs.getTimestamp("created"))) + .createdBy(rs.getString("created_by")) + .lastModified(DbUtils.timestampToOffsetDateTime(rs.getTimestamp("last_modified"))); // SQL query and row mapper for reading a review instance. private static final String REVIEW_INSTANCE_SELECT_SQL = @@ -79,8 +79,8 @@ public void createReview( cohortDao.freezeCohortLatestVersionOrThrow(studyId, cohort.getCohortRevisionGroupId()); final String sql = - "INSERT INTO review (cohort_id, review_id, display_name, description, size, created) " - + "VALUES (:cohort_id, :review_id, :display_name, :description, :size, :created)"; + "INSERT INTO review (cohort_id, review_id, display_name, description, size, created, created_by) " + + "VALUES (:cohort_id, :review_id, :display_name, :description, :size, :created, :created_by)"; MapSqlParameterSource params = new MapSqlParameterSource() .addValue("cohort_id", cohort.getCohortId()) @@ -88,7 +88,8 @@ public void createReview( .addValue("display_name", review.getDisplayName()) .addValue("description", review.getDescription()) .addValue("size", review.getSize()) - .addValue("created", Timestamp.from(Instant.now())); + // Don't need to set created. Liquibase defaultValueComputed handles that. + .addValue("created_by", review.getCreatedBy()); try { jdbcTemplate.update(sql, params); LOGGER.info("Inserted record for review {}", review.getReviewId()); diff --git a/service/src/main/java/bio/terra/tanagra/db/StudyDao.java b/service/src/main/java/bio/terra/tanagra/db/StudyDao.java index 77c8a6162..d8f47258f 100644 --- a/service/src/main/java/bio/terra/tanagra/db/StudyDao.java +++ b/service/src/main/java/bio/terra/tanagra/db/StudyDao.java @@ -30,7 +30,7 @@ public class StudyDao { // SQL query and row mapper for reading a study. private static final String STUDY_SELECT_SQL = - "SELECT study_id, display_name, description, properties FROM study"; + "SELECT study_id, display_name, description, properties, created, created_by, last_modified FROM study"; private static final RowMapper STUDY_ROW_MAPPER = (rs, rowNum) -> Study.builder() @@ -41,6 +41,9 @@ public class StudyDao { Optional.ofNullable(rs.getString("properties")) .map(DbSerDes::jsonToProperties) .orElse(null)) + .created(DbUtils.timestampToOffsetDateTime(rs.getTimestamp("created"))) + .createdBy(rs.getString("created_by")) + .lastModified(DbUtils.timestampToOffsetDateTime(rs.getTimestamp("last_modified"))) .build(); private final NamedParameterJdbcTemplate jdbcTemplate; @@ -58,15 +61,17 @@ public StudyDao(NamedParameterJdbcTemplate jdbcTemplate) { @WriteTransaction public void createStudy(Study study) { final String sql = - "INSERT INTO study (study_id, display_name, description, properties) " - + "VALUES (:study_id, :display_name, :description, CAST(:properties AS jsonb))"; + "INSERT INTO study (study_id, display_name, description, properties, created_by) " + + "VALUES (:study_id, :display_name, :description, CAST(:properties AS jsonb), :created_by)"; MapSqlParameterSource params = new MapSqlParameterSource() .addValue("study_id", study.getStudyId()) .addValue("display_name", study.getDisplayName()) .addValue("description", study.getDescription()) - .addValue("properties", DbSerDes.propertiesToJson(study.getProperties())); + .addValue("properties", DbSerDes.propertiesToJson(study.getProperties())) + // Don't need to set created. Liquibase defaultValueComputed handles that. + .addValue("created_by", study.getCreatedBy()); try { jdbcTemplate.update(sql, params); LOGGER.info("Inserted record for study {}", study.getStudyId()); diff --git a/service/src/main/java/bio/terra/tanagra/service/artifact/Cohort.java b/service/src/main/java/bio/terra/tanagra/service/artifact/Cohort.java index d00c04c97..edfbbeaaf 100644 --- a/service/src/main/java/bio/terra/tanagra/service/artifact/Cohort.java +++ b/service/src/main/java/bio/terra/tanagra/service/artifact/Cohort.java @@ -1,9 +1,7 @@ package bio.terra.tanagra.service.artifact; import bio.terra.tanagra.exception.SystemException; -import java.sql.Timestamp; import java.time.OffsetDateTime; -import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -19,7 +17,9 @@ public class Cohort { private final int version; private final boolean isMostRecent; private final boolean isEditable; - private final Timestamp lastModified; + private final OffsetDateTime created; + private final String createdBy; + private final OffsetDateTime lastModified; private final @Nullable String displayName; private final @Nullable String description; private final List criteriaGroups; @@ -32,6 +32,8 @@ private Cohort(Builder builder) { this.version = builder.version; this.isMostRecent = builder.isMostRecent; this.isEditable = builder.isEditable; + this.created = builder.created; + this.createdBy = builder.createdBy; this.lastModified = builder.lastModified; this.displayName = builder.displayName; this.description = builder.description; @@ -51,6 +53,8 @@ public Builder toBuilder() { .version(version) .isMostRecent(isMostRecent) .isEditable(isEditable) + .created(created) + .createdBy(createdBy) .lastModified(lastModified) .displayName(displayName) .description(description) @@ -98,9 +102,19 @@ public boolean isEditable() { return isEditable; } + /** Timestamp of when this cohort was created. */ + public OffsetDateTime getCreated() { + return created; + } + + /** Email of user who created this cohort. */ + public String getCreatedBy() { + return createdBy; + } + /** Timestamp of when this cohort was last modified. */ - public OffsetDateTime getLastModifiedUTC() { - return lastModified.toInstant().atOffset(ZoneOffset.UTC); + public OffsetDateTime getLastModified() { + return lastModified; } /** Optional display name for the cohort. */ @@ -130,7 +144,9 @@ public static class Builder { private int version; private boolean isMostRecent; private boolean isEditable; - private Timestamp lastModified; + private OffsetDateTime created; + private String createdBy; + private OffsetDateTime lastModified; private @Nullable String displayName; private @Nullable String description; private List criteriaGroups = new ArrayList<>(); @@ -170,8 +186,18 @@ public Builder isEditable(boolean isEditable) { return this; } - public Builder lastModified(Timestamp lastModified) { - this.lastModified = (Timestamp) lastModified.clone(); + public Builder created(OffsetDateTime created) { + this.created = created; + return this; + } + + public Builder createdBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public Builder lastModified(OffsetDateTime lastModified) { + this.lastModified = lastModified; return this; } diff --git a/service/src/main/java/bio/terra/tanagra/service/artifact/ConceptSet.java b/service/src/main/java/bio/terra/tanagra/service/artifact/ConceptSet.java index 9e5505d6b..e36bef3ab 100644 --- a/service/src/main/java/bio/terra/tanagra/service/artifact/ConceptSet.java +++ b/service/src/main/java/bio/terra/tanagra/service/artifact/ConceptSet.java @@ -1,9 +1,7 @@ package bio.terra.tanagra.service.artifact; import bio.terra.tanagra.exception.SystemException; -import java.sql.Timestamp; import java.time.OffsetDateTime; -import java.time.ZoneOffset; import javax.annotation.Nullable; public class ConceptSet { @@ -11,7 +9,9 @@ public class ConceptSet { private final String conceptSetId; private final String underlayName; private final String entityName; - private final Timestamp lastModified; + private final OffsetDateTime created; + private final String createdBy; + private final OffsetDateTime lastModified; private final @Nullable String displayName; private final @Nullable String description; private final Criteria criteria; @@ -21,6 +21,8 @@ private ConceptSet(Builder builder) { this.conceptSetId = builder.conceptSetId; this.underlayName = builder.underlayName; this.entityName = builder.entityName; + this.created = builder.created; + this.createdBy = builder.createdBy; this.lastModified = builder.lastModified; this.displayName = builder.displayName; this.description = builder.description; @@ -37,6 +39,8 @@ public Builder toBuilder() { .conceptSetId(conceptSetId) .underlayName(underlayName) .entityName(entityName) + .created(created) + .createdBy(createdBy) .lastModified(lastModified) .displayName(displayName) .description(description) @@ -63,9 +67,19 @@ public String getEntityName() { return entityName; } + /** Timestamp of when this concept set was created. */ + public OffsetDateTime getCreated() { + return created; + } + + /** Email of user who created this concept set. */ + public String getCreatedBy() { + return createdBy; + } + /** Timestamp of when this concept set was last modified. */ - public OffsetDateTime getLastModifiedUTC() { - return lastModified.toInstant().atOffset(ZoneOffset.UTC); + public OffsetDateTime getLastModified() { + return lastModified; } /** Optional display name for the concept set. */ @@ -88,7 +102,9 @@ public static class Builder { private String conceptSetId; private String underlayName; private String entityName; - private Timestamp lastModified; + private OffsetDateTime created; + private String createdBy; + private OffsetDateTime lastModified; private @Nullable String displayName; private @Nullable String description; private Criteria criteria; @@ -113,8 +129,18 @@ public Builder entityName(String entityName) { return this; } - public Builder lastModified(Timestamp lastModified) { - this.lastModified = (Timestamp) lastModified.clone(); + public Builder created(OffsetDateTime created) { + this.created = created; + return this; + } + + public Builder createdBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public Builder lastModified(OffsetDateTime lastModified) { + this.lastModified = lastModified; return this; } diff --git a/service/src/main/java/bio/terra/tanagra/service/artifact/Review.java b/service/src/main/java/bio/terra/tanagra/service/artifact/Review.java index ae858ba5a..024da1c07 100644 --- a/service/src/main/java/bio/terra/tanagra/service/artifact/Review.java +++ b/service/src/main/java/bio/terra/tanagra/service/artifact/Review.java @@ -1,8 +1,6 @@ package bio.terra.tanagra.service.artifact; -import java.sql.Timestamp; import java.time.OffsetDateTime; -import java.time.ZoneOffset; import javax.annotation.Nullable; public class Review { @@ -11,7 +9,9 @@ public class Review { private final @Nullable String displayName; private final @Nullable String description; private final int size; - private final Timestamp created; + private final OffsetDateTime created; + private final String createdBy; + private final OffsetDateTime lastModified; private final Cohort cohort; private Review(Builder builder) { @@ -21,6 +21,8 @@ private Review(Builder builder) { this.description = builder.description; this.size = builder.size; this.created = builder.created; + this.createdBy = builder.createdBy; + this.lastModified = builder.lastModified; this.cohort = builder.cohort; } @@ -53,9 +55,16 @@ public int getSize() { return size; } - /** Timestamp of when this review was last modified. */ - public OffsetDateTime getCreatedUTC() { - return created.toInstant().atOffset(ZoneOffset.UTC); + public OffsetDateTime getCreated() { + return created; + } + + public String getCreatedBy() { + return createdBy; + } + + public OffsetDateTime getLastModified() { + return lastModified; } /** Cohort revision that this review is pinned to. */ @@ -69,7 +78,9 @@ public static class Builder { private @Nullable String displayName; private @Nullable String description; private int size; - private Timestamp created; + private OffsetDateTime created; + private String createdBy; + private OffsetDateTime lastModified; private Cohort cohort; public Builder cohortId(String cohortId) { @@ -97,8 +108,18 @@ public Builder size(int size) { return this; } - public Builder created(Timestamp created) { - this.created = (Timestamp) created.clone(); + public Builder created(OffsetDateTime created) { + this.created = created; + return this; + } + + public Builder createdBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public Builder lastModified(OffsetDateTime lastModified) { + this.lastModified = lastModified; return this; } diff --git a/service/src/main/java/bio/terra/tanagra/service/artifact/Study.java b/service/src/main/java/bio/terra/tanagra/service/artifact/Study.java index f4988a356..842a26659 100644 --- a/service/src/main/java/bio/terra/tanagra/service/artifact/Study.java +++ b/service/src/main/java/bio/terra/tanagra/service/artifact/Study.java @@ -3,6 +3,7 @@ import bio.terra.tanagra.exception.SystemException; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import java.time.OffsetDateTime; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; @@ -20,16 +21,25 @@ public class Study { private final @Nullable String displayName; private final @Nullable String description; private final Map properties; + private final OffsetDateTime created; + private final String createdBy; + private final OffsetDateTime lastModified; public Study( String studyId, @Nullable String displayName, @Nullable String description, - Map properties) { + Map properties, + OffsetDateTime created, + String createdBy, + OffsetDateTime lastModified) { this.studyId = studyId; this.displayName = displayName; this.description = description; this.properties = properties; + this.created = created; + this.createdBy = createdBy; + this.lastModified = lastModified; } /** The globally unique identifier of this study. */ @@ -52,6 +62,18 @@ public Map getProperties() { return properties; } + public OffsetDateTime getCreated() { + return created; + } + + public String getCreatedBy() { + return createdBy; + } + + public OffsetDateTime getLastModified() { + return lastModified; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -69,6 +91,9 @@ public boolean equals(Object o) { .append(displayName, study.displayName) .append(description, study.description) .append(properties, study.properties) + .append(created, study.created) + .append(createdBy, study.createdBy) + .append(lastModified, study.lastModified) .isEquals(); } @@ -79,6 +104,9 @@ public int hashCode() { .append(displayName) .append(description) .append(properties) + .append(created) + .append(createdBy) + .append(lastModified) .toHashCode(); } @@ -92,6 +120,9 @@ public static class Builder { private @Nullable String displayName; private String description; private Map properties; + private OffsetDateTime created; + private String createdBy; + private OffsetDateTime lastModified; public Builder studyId(String studyId) { this.studyId = studyId; @@ -113,6 +144,21 @@ public Builder properties(Map properties) { return this; } + public Builder created(OffsetDateTime created) { + this.created = created; + return this; + } + + public Builder createdBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public Builder lastModified(OffsetDateTime lastModified) { + this.lastModified = lastModified; + return this; + } + public Study build() { // Always have a map, even if it is empty if (properties == null) { @@ -121,7 +167,8 @@ public Study build() { if (studyId == null) { throw new SystemException("Study requires id"); } - return new Study(studyId, displayName, description, properties); + return new Study( + studyId, displayName, description, properties, created, createdBy, lastModified); } } } diff --git a/service/src/main/java/bio/terra/tanagra/service/utils/ToApiConversionUtils.java b/service/src/main/java/bio/terra/tanagra/service/utils/ToApiConversionUtils.java index 529ed048c..799ceed13 100644 --- a/service/src/main/java/bio/terra/tanagra/service/utils/ToApiConversionUtils.java +++ b/service/src/main/java/bio/terra/tanagra/service/utils/ToApiConversionUtils.java @@ -70,7 +70,9 @@ public static ApiCohortV2 toApiObject(Cohort cohort) { .underlayName(cohort.getUnderlayName()) .displayName(cohort.getDisplayName()) .description(cohort.getDescription()) - .lastModified(cohort.getLastModifiedUTC()) + .created(cohort.getCreated()) + .createdBy(cohort.getCreatedBy()) + .lastModified(cohort.getLastModified()) .criteriaGroups( cohort.getCriteriaGroups().stream() .map(criteriaGroup -> toApiObject(criteriaGroup)) diff --git a/service/src/main/resources/api/service_openapi.yaml b/service/src/main/resources/api/service_openapi.yaml index d38dfef28..10ea0f823 100644 --- a/service/src/main/resources/api/service_openapi.yaml +++ b/service/src/main/resources/api/service_openapi.yaml @@ -1542,7 +1542,7 @@ components: StudyV2: type: object - required: [id] + required: [id, created, createdBy, lastModified] properties: id: description: ID of the study, immutable @@ -1553,6 +1553,15 @@ components: $ref: "#/components/schemas/StudyDescriptionV2" properties: $ref: "#/components/schemas/PropertiesV2" + created: + type: string + format: date-time + createdBy: + description: Email of user who created the study + type: string + lastModified: + type: string + format: date-time StudyListV2: type: array @@ -1624,6 +1633,13 @@ components: description: Groups of criteria that define the entity filter items: $ref: "#/components/schemas/CriteriaGroupV2" + created: + description: Timestamp of when the cohort was created + type: string + format: date-time + createdBy: + description: Email of user who created cohort + type: string lastModified: description: Timestamp of when the cohort was last modified type: string @@ -1633,6 +1649,8 @@ components: - underlayName - displayName - criteriaGroups + - created + - createdBy - lastModified CohortListV2: @@ -1687,6 +1705,13 @@ components: $ref: "#/components/schemas/ConceptSetDescriptionV2" criteria: $ref: "#/components/schemas/CriteriaV2" + created: + description: Timestamp of when the concept set was created + type: string + format: date-time + createdBy: + description: Email of user who created the concept set + type: string lastModified: description: Timestamp of when the concept set was last modified type: string @@ -1697,6 +1722,8 @@ components: - entity - displayName - criteria + - created + - createdBy - lastModified ConceptSetListV2: @@ -1819,12 +1846,21 @@ components: description: Timestamp of when the review was created type: string format: date-time + createdBy: + description: Email of user who created the review + type: string + lastModified: + description: Timestamp of when the review was last modified + type: string + format: date-time required: - id - displayName - size - cohortRevision - created + - createdBy + - lastModified ReviewListV2: type: array @@ -2010,4 +2046,4 @@ components: profile: profile authorization bearerAuth: type: http - scheme: bearer \ No newline at end of file + scheme: bearer diff --git a/service/src/main/resources/db/changelog.xml b/service/src/main/resources/db/changelog.xml index 62283b5e2..5967e2e0b 100644 --- a/service/src/main/resources/db/changelog.xml +++ b/service/src/main/resources/db/changelog.xml @@ -6,4 +6,5 @@ - \ No newline at end of file + + diff --git a/service/src/main/resources/db/changesets/20221219_created_columns.yaml b/service/src/main/resources/db/changesets/20221219_created_columns.yaml new file mode 100644 index 000000000..ff54232e5 --- /dev/null +++ b/service/src/main/resources/db/changesets/20221219_created_columns.yaml @@ -0,0 +1,149 @@ +databaseChangeLog: +- changeSet: + id: created_columns + author: melchang + changes: + # Add created, created_by, last_modified columns + - addColumn: + tableName: cohort + columns: + - column: + name: created + type: timestamptz + constraints: + nullable: false + # Needed to backfill rows that existed before this changeset. + # Will also set for new rows. + defaultValueComputed: now() + - addColumn: + tableName: concept_set + columns: + - column: + name: created + type: timestamptz + constraints: + nullable: false + # Needed to backfill rows that existed before this changeset. + # Will also set for new rows. + defaultValueComputed: now() + - addColumn: + tableName: study + columns: + - column: + name: created + type: timestamptz + constraints: + nullable: false + # Needed to backfill rows that existed before this changeset. + # Will also set for new rows. + defaultValueComputed: now() + - addColumn: + tableName: review + columns: + - column: + name: last_modified + type: timestamptz + constraints: + nullable: false + # Needed to backfill rows that existed before this changeset. + # Will also set for new rows. + defaultValueComputed: now() + - addColumn: + tableName: study + columns: + - column: + name: last_modified + type: timestamptz + constraints: + nullable: false + # Needed to backfill rows that existed before this changeset. + # Will also set for new rows. + defaultValueComputed: now() + # We want existing rows to have "unknown" and new rows to not have + # "unknown". Unfortunately this takes 3 steps. See + # https://stackoverflow.com/a/8906534/6447189 + - addColumn: + tableName: cohort + columns: + - column: + name: created_by + type: text + - update: + tableName: cohort + columns: + - column: + name: created_by + value: "unknown" + - addNotNullConstraint: + tableName: cohort + columnName: created_by + - addColumn: + tableName: concept_set + columns: + - column: + name: created_by + type: text + - update: + tableName: concept_set + columns: + - column: + name: created_by + value: "unknown" + - addNotNullConstraint: + tableName: concept_set + columnName: created_by + - addColumn: + tableName: review + columns: + - column: + name: created_by + type: text + - update: + tableName: review + columns: + - column: + name: created_by + value: "unknown" + - addNotNullConstraint: + tableName: review + columnName: created_by + - addColumn: + tableName: study + columns: + - column: + name: created_by + type: text + - update: + tableName: study + columns: + - column: + name: created_by + value: "unknown" + - addNotNullConstraint: + tableName: study + columnName: created_by + + # Change existing columns from timestamp to timestamptz + - modifyDataType: + tableName: cohort + columnName: last_modified + newDataType: timestamptz + - modifyDataType: + tableName: concept_set + columnName: last_modified + newDataType: timestamptz + - modifyDataType: + tableName: review + columnName: created + newDataType: timestamptz + + # Add not null constraint to existing columns + - addNotNullConstraint: + tableName: cohort + columnName: last_modified + - addNotNullConstraint: + tableName: concept_set + columnName: last_modified + - addNotNullConstraint: + tableName: review + columnName: created diff --git a/ui/src/data/source.tsx b/ui/src/data/source.tsx index 0bbb473b4..94e0901e4 100644 --- a/ui/src/data/source.tsx +++ b/ui/src/data/source.tsx @@ -465,6 +465,8 @@ export class BackendSource implements Source { size, cohort: toAPICohort(cohort), created: new Date(), + createdBy: "", + lastModified: new Date(), }; const reviews = loadLocalReviews(); @@ -1066,6 +1068,8 @@ function toAPICohort(cohort: tanagra.Cohort): tanagra.CohortV2 { id: cohort.id, displayName: cohort.name, underlayName: cohort.underlayName, + created: new Date(), + createdBy: "", lastModified: new Date(), criteriaGroups: cohort.groups.map((group) => ({ id: group.id, diff --git a/ui/src/sd-admin/studyAdmin.tsx b/ui/src/sd-admin/studyAdmin.tsx index 929c2eb6a..cef1f6bda 100644 --- a/ui/src/sd-admin/studyAdmin.tsx +++ b/ui/src/sd-admin/studyAdmin.tsx @@ -118,6 +118,9 @@ const emptyStudy: StudyV2 = { { key: "irbNumber", value: "" }, { key: "pi", value: "" }, ], + created: new Date(), + createdBy: "user@gmail.com", + lastModified: new Date(), }; const initialFormState = {