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 = {